pdk 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/LICENSE +201 -0
- data/README.md +270 -0
- data/exe/pdk +5 -0
- data/lib/pdk.rb +8 -0
- data/lib/pdk/cli.rb +80 -0
- data/lib/pdk/cli/errors.rb +12 -0
- data/lib/pdk/cli/exec.rb +51 -0
- data/lib/pdk/cli/input.rb +28 -0
- data/lib/pdk/cli/new.rb +30 -0
- data/lib/pdk/cli/new/class.rb +45 -0
- data/lib/pdk/cli/new/module.rb +55 -0
- data/lib/pdk/cli/test.rb +23 -0
- data/lib/pdk/cli/tests/unit.rb +52 -0
- data/lib/pdk/cli/util/option_normalizer.rb +44 -0
- data/lib/pdk/cli/util/option_validator.rb +74 -0
- data/lib/pdk/cli/validate.rb +81 -0
- data/lib/pdk/generate.rb +3 -0
- data/lib/pdk/generators/module.rb +139 -0
- data/lib/pdk/generators/puppet_class.rb +51 -0
- data/lib/pdk/generators/puppet_object.rb +213 -0
- data/lib/pdk/i18n.rb +4 -0
- data/lib/pdk/logger.rb +25 -0
- data/lib/pdk/module/metadata.rb +88 -0
- data/lib/pdk/module/templatedir.rb +231 -0
- data/lib/pdk/report.rb +38 -0
- data/lib/pdk/template_file.rb +87 -0
- data/lib/pdk/tests/unit.rb +21 -0
- data/lib/pdk/util.rb +16 -0
- data/lib/pdk/validate.rb +12 -0
- data/lib/pdk/validators/base_validator.rb +14 -0
- data/lib/pdk/validators/metadata.rb +17 -0
- data/lib/pdk/validators/puppet_lint.rb +17 -0
- data/lib/pdk/validators/puppet_parser.rb +17 -0
- data/lib/pdk/validators/ruby_lint.rb +17 -0
- data/lib/pdk/version.rb +3 -0
- data/locales/config.yaml +21 -0
- data/locales/de/pdk.po +250 -0
- data/locales/pdk.pot +215 -0
- metadata +92 -12
data/lib/pdk/i18n.rb
ADDED
data/lib/pdk/logger.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module PDK
|
4
|
+
def self.logger
|
5
|
+
@logger ||= PDK::Logger.new
|
6
|
+
end
|
7
|
+
|
8
|
+
class Logger < ::Logger
|
9
|
+
def initialize
|
10
|
+
# TODO: Decide where log output goes, probably stderr?
|
11
|
+
super(STDOUT)
|
12
|
+
|
13
|
+
# TODO: Decide on output format.
|
14
|
+
self.formatter = proc do |severity,datetime,progname,msg|
|
15
|
+
"pdk (#{severity}): #{msg}\n"
|
16
|
+
end
|
17
|
+
|
18
|
+
self.level = ::Logger::INFO
|
19
|
+
end
|
20
|
+
|
21
|
+
def enable_debug_output
|
22
|
+
self.level = ::Logger::DEBUG
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module PDK
|
4
|
+
module Module
|
5
|
+
class Metadata
|
6
|
+
|
7
|
+
attr_accessor :data
|
8
|
+
|
9
|
+
DEFAULTS = {
|
10
|
+
'name' => nil,
|
11
|
+
'version' => nil,
|
12
|
+
'author' => nil,
|
13
|
+
'summary' => nil,
|
14
|
+
'license' => 'Apache-2.0',
|
15
|
+
'source' => '',
|
16
|
+
'project_page' => nil,
|
17
|
+
'issues_url' => nil,
|
18
|
+
'dependencies' => Set.new.freeze,
|
19
|
+
'data_provider' => nil,
|
20
|
+
}
|
21
|
+
|
22
|
+
def initialize(params = {})
|
23
|
+
@data = DEFAULTS.dup
|
24
|
+
update!(params) if params
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.from_file(metadata_json_path)
|
28
|
+
unless File.file?(metadata_json_path)
|
29
|
+
raise ArgumentError, _("'%{file}' does not exist or is not a file") % {file: metadata_json_path}
|
30
|
+
end
|
31
|
+
|
32
|
+
unless File.readable?(metadata_json_path)
|
33
|
+
raise ArgumentError, _("Unable to open '%{file}' for reading") % {file: metadata_json_path}
|
34
|
+
end
|
35
|
+
|
36
|
+
begin
|
37
|
+
data = JSON.parse(File.read(metadata_json_path))
|
38
|
+
rescue JSON::JSONError => e
|
39
|
+
raise ArgumentError, _("Invalid JSON in metadata.json: %{msg}") % {msg: e.message}
|
40
|
+
end
|
41
|
+
|
42
|
+
new(data)
|
43
|
+
end
|
44
|
+
|
45
|
+
def update!(data)
|
46
|
+
# TODO: validate all data
|
47
|
+
process_name(data) if data['name']
|
48
|
+
@data.merge!(data)
|
49
|
+
self
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_json
|
53
|
+
JSON.pretty_generate(@data)
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
# Do basic validation and parsing of the name parameter.
|
59
|
+
def process_name(data)
|
60
|
+
validate_name(data['name'])
|
61
|
+
author, module_name = data['name'].split(/[-\/]/, 2)
|
62
|
+
|
63
|
+
data['author'] ||= author if @data['author'] == DEFAULTS['author']
|
64
|
+
end
|
65
|
+
|
66
|
+
# Validates that the given module name is both namespaced and well-formed.
|
67
|
+
def validate_name(name)
|
68
|
+
return if name =~ /\A[a-z0-9]+[-\/][a-z][a-z0-9_]*\Z/i
|
69
|
+
|
70
|
+
namespace, modname = name.split(/[-\/]/, 2)
|
71
|
+
modname = :namespace_missing if namespace == ''
|
72
|
+
|
73
|
+
err = case modname
|
74
|
+
when nil, '', :namespace_missing
|
75
|
+
"the field must be a dash-separated username and module name"
|
76
|
+
when /[^a-z0-9_]/i
|
77
|
+
"the module name contains non-alphanumeric (or underscore) characters"
|
78
|
+
when /^[^a-z]/i
|
79
|
+
"the module name must begin with a letter"
|
80
|
+
else
|
81
|
+
"the namespace contains non-alphanumeric characters"
|
82
|
+
end
|
83
|
+
|
84
|
+
raise ArgumentError, "Invalid 'name' field in metadata.json: #{err}"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,231 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'pdk/util'
|
3
|
+
require 'pdk/cli/exec'
|
4
|
+
require 'pdk/cli/errors'
|
5
|
+
require 'pdk/template_file'
|
6
|
+
|
7
|
+
module PDK
|
8
|
+
module Module
|
9
|
+
class TemplateDir
|
10
|
+
# Initialises the TemplateDir object with the path or URL to the template
|
11
|
+
# and the block of code to run to be run while the template is available.
|
12
|
+
#
|
13
|
+
# The template directory is only guaranteed to be available on disk
|
14
|
+
# within the scope of the block passed to this method.
|
15
|
+
#
|
16
|
+
# @param path_or_url [String] The path to a directory to use as the
|
17
|
+
# template or a URL to a git repository.
|
18
|
+
# @yieldparam self [PDK::Module::TemplateDir] The initialised object with
|
19
|
+
# the template available on disk.
|
20
|
+
#
|
21
|
+
# @example Using a git repository as a template
|
22
|
+
# PDK::Module::TemplateDir.new('https://github.com/puppetlabs/pdk-module-template') do |t|
|
23
|
+
# t.render do |filename, content|
|
24
|
+
# File.open(filename, 'w') do |file|
|
25
|
+
# file.write(content)
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# @raise [PDK::CLI::FatalError] If the template is a git repository and
|
31
|
+
# the git binary is unavailable.
|
32
|
+
# @raise [PDK::CLI::FatalError] If the template is a git repository and
|
33
|
+
# the git clone operation fails.
|
34
|
+
# @raise [ArgumentError] (see #validate_module_template!)
|
35
|
+
#
|
36
|
+
# @api public
|
37
|
+
def initialize(path_or_url, &block)
|
38
|
+
if File.directory?(path_or_url)
|
39
|
+
@path = path_or_url
|
40
|
+
else
|
41
|
+
# If path_or_url isn't a directory on disk, we assume that it is
|
42
|
+
# a remote git repository.
|
43
|
+
|
44
|
+
# @todo When switching this over to using rugged, cache the cloned
|
45
|
+
# template repo in `%AppData%` or `$XDG_CACHE_DIR` and update before
|
46
|
+
# use.
|
47
|
+
temp_dir = PDK::Util.make_tmpdir_name('pdk-module-template')
|
48
|
+
|
49
|
+
clone_result = PDK::CLI::Exec.git('clone', path_or_url, temp_dir)
|
50
|
+
unless clone_result[:exit_code] == 0
|
51
|
+
PDK.logger.error clone_result[:stdout]
|
52
|
+
PDK.logger.error clone_result[:stderr]
|
53
|
+
raise PDK::CLI::FatalError, _("Unable to clone git repository '%{repo}' to '%{dest}'") % {:repo => path_or_url, :dest => temp_dir}
|
54
|
+
end
|
55
|
+
@path = temp_dir
|
56
|
+
@repo = path_or_url
|
57
|
+
end
|
58
|
+
|
59
|
+
@moduleroot_dir = File.join(@path, 'moduleroot')
|
60
|
+
@object_dir = File.join(@path, 'object_templates')
|
61
|
+
validate_module_template!
|
62
|
+
|
63
|
+
yield self
|
64
|
+
ensure
|
65
|
+
# If we cloned a git repo to get the template, remove the clone once
|
66
|
+
# we're done with it.
|
67
|
+
if @repo
|
68
|
+
FileUtils.remove_dir(@path)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Retrieve identifying metadata for the template.
|
73
|
+
#
|
74
|
+
# For git repositories, this will return the URL to the repository and
|
75
|
+
# a reference to the HEAD.
|
76
|
+
#
|
77
|
+
# @return [Hash{String => String}] A hash of identifying metadata.
|
78
|
+
#
|
79
|
+
# @api public
|
80
|
+
def metadata
|
81
|
+
if @repo
|
82
|
+
ref_result = PDK::CLI::Exec.git('--git-dir', File.join(@path, '.git'), 'describe', '--all', '--long')
|
83
|
+
if ref_result[:exit_code] == 0
|
84
|
+
{'template-url' => @repo, 'template-ref' => ref_result[:stdout].strip}
|
85
|
+
else
|
86
|
+
{}
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Loop through the files in the template, yielding each rendered file to
|
92
|
+
# the supplied block.
|
93
|
+
#
|
94
|
+
# @yieldparam dest_path [String] The path of the destination file,
|
95
|
+
# relative to the root of the module.
|
96
|
+
# @yieldparam dest_content [String] The rendered content of the
|
97
|
+
# destination file.
|
98
|
+
#
|
99
|
+
# @raise [PDK::CLI::FatalError] If the template fails to render.
|
100
|
+
#
|
101
|
+
# @return [void]
|
102
|
+
#
|
103
|
+
# @api public
|
104
|
+
def render(&block)
|
105
|
+
files_in_template.each do |template_file|
|
106
|
+
PDK.logger.debug(_("Rendering '%{template}'...") % {:template => template_file})
|
107
|
+
dest_path = template_file.sub(/\.erb\Z/, '')
|
108
|
+
|
109
|
+
begin
|
110
|
+
dest_content = PDK::TemplateFile.new(File.join(@moduleroot_dir, template_file), {:configs => config_for(dest_path)}).render
|
111
|
+
rescue => e
|
112
|
+
error_msg = _(
|
113
|
+
"Failed to render template '%{template}'\n" +
|
114
|
+
"%{exception}: %{message}"
|
115
|
+
) % {:template => template_file, :exception => e.class, :message => e.message}
|
116
|
+
raise PDK::CLI::FatalError, error_msg
|
117
|
+
end
|
118
|
+
|
119
|
+
yield dest_path, dest_content
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# Searches the template directory for template files that can be used to
|
124
|
+
# render files for the specified object type.
|
125
|
+
#
|
126
|
+
# @param object_type [Symbol] The object type, e.g. (`:class`,
|
127
|
+
# `:defined_type`, `:fact`, etc).
|
128
|
+
#
|
129
|
+
# @return [Hash{Symbol => String}] if the templates are available in the
|
130
|
+
# template dir, otherwise `nil`. The returned hash can contain two keys,
|
131
|
+
# :object contains the path on disk to the template for the object, :spec
|
132
|
+
# contains the path on disk to the template for the object's spec file
|
133
|
+
# (if available).
|
134
|
+
#
|
135
|
+
# @api public
|
136
|
+
def object_template_for(object_type)
|
137
|
+
object_path = File.join(@object_dir, "#{object_type.to_s}.erb")
|
138
|
+
spec_path = File.join(@object_dir, "#{object_type.to_s}_spec.erb")
|
139
|
+
|
140
|
+
if File.file?(object_path) && File.readable?(object_path)
|
141
|
+
result = {object: object_path}
|
142
|
+
result[:spec] = spec_path if File.file?(spec_path) && File.readable?(spec_path)
|
143
|
+
result
|
144
|
+
else
|
145
|
+
nil
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# Generate a hash of data to be used when rendering object templates.
|
150
|
+
#
|
151
|
+
# Read `config_defaults.yml` from the root of the template directory (if
|
152
|
+
# it exists) build a hash of values from the value of the `:global`
|
153
|
+
# key.
|
154
|
+
#
|
155
|
+
# @return [Hash] The data that will be available to the template via the
|
156
|
+
# `@configs` instance variable.
|
157
|
+
#
|
158
|
+
# @api private
|
159
|
+
def object_config
|
160
|
+
config_for(nil)
|
161
|
+
end
|
162
|
+
private
|
163
|
+
# Validate the content of the template directory.
|
164
|
+
#
|
165
|
+
# @raise [ArgumentError] If the specified path is not a directory.
|
166
|
+
# @raise [ArgumentError] If the template directory does not contain
|
167
|
+
# a directory called 'moduleroot'.
|
168
|
+
#
|
169
|
+
# @return [void]
|
170
|
+
#
|
171
|
+
# @api private
|
172
|
+
def validate_module_template!
|
173
|
+
unless File.directory?(@path)
|
174
|
+
raise ArgumentError, _("The specified template '%{path}' is not a directory") % {:path => @path}
|
175
|
+
end
|
176
|
+
|
177
|
+
unless File.directory?(@moduleroot_dir)
|
178
|
+
raise ArgumentError, _("The template at '%{path}' does not contain a moduleroot directory") % {:path => @path}
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
# Get a list of template files in the template directory.
|
183
|
+
#
|
184
|
+
# @return [Array[String]] An array of file names, relative to the
|
185
|
+
# `moduleroot` directory.
|
186
|
+
#
|
187
|
+
# @api private
|
188
|
+
def files_in_template
|
189
|
+
@files ||= Dir.glob(File.join(@moduleroot_dir, "**", "*"), File::FNM_DOTMATCH).select { |template_path|
|
190
|
+
File.file?(template_path) && !File.symlink?(template_path)
|
191
|
+
}.map { |template_path|
|
192
|
+
template_path.sub(/\A#{Regexp.escape(@moduleroot_dir)}#{Regexp.escape(File::SEPARATOR)}/, '')
|
193
|
+
}
|
194
|
+
end
|
195
|
+
|
196
|
+
# Generate a hash of data to be used when rendering the specified
|
197
|
+
# template.
|
198
|
+
#
|
199
|
+
# Read `config_defaults.yml` from the root of the template directory (if
|
200
|
+
# it exists) build a hash of values by merging the value of the `:global`
|
201
|
+
# key with the value of the key that matches `dest_path`.
|
202
|
+
#
|
203
|
+
# @param dest_path [String] The destination path of the file that the
|
204
|
+
# data is for, relative to the root of the module.
|
205
|
+
#
|
206
|
+
# @return [Hash] The data that will be available to the template via the
|
207
|
+
# `@configs` instance variable.
|
208
|
+
#
|
209
|
+
# @api private
|
210
|
+
def config_for(dest_path)
|
211
|
+
if @config.nil?
|
212
|
+
config_path = File.join(@path, 'config_defaults.yml')
|
213
|
+
|
214
|
+
if File.file?(config_path) && File.readable?(config_path)
|
215
|
+
begin
|
216
|
+
@config = YAML.load(File.read(config_path))
|
217
|
+
rescue
|
218
|
+
PDK.logger.warn(_("'%{file}' is not a valid YAML file") % {:file => config_path})
|
219
|
+
@config = {}
|
220
|
+
end
|
221
|
+
else
|
222
|
+
@config = {}
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
file_config = @config.fetch(:global, {})
|
227
|
+
file_config.merge(@config.fetch(dest_path, {})) unless dest_path.nil?
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
data/lib/pdk/report.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
module PDK
|
2
|
+
class Report
|
3
|
+
def initialize(path, format = nil)
|
4
|
+
@path = path
|
5
|
+
@format = format || self.class.default_format
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.formats
|
9
|
+
@report_formats ||= ['junit', 'text'].freeze
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.default_format
|
13
|
+
'junit'
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.default_target
|
17
|
+
'stdout' # TODO: actually write to stdout
|
18
|
+
end
|
19
|
+
|
20
|
+
def write(text)
|
21
|
+
if @format == 'junit'
|
22
|
+
report = prepare_junit(text)
|
23
|
+
elsif @format == 'text'
|
24
|
+
report = prepare_text(text)
|
25
|
+
end
|
26
|
+
|
27
|
+
File.open(@path, 'a') { |f| f.write(report) }
|
28
|
+
end
|
29
|
+
|
30
|
+
def prepare_junit(text)
|
31
|
+
"junit: #{text}"
|
32
|
+
end
|
33
|
+
|
34
|
+
def prepare_text(text)
|
35
|
+
"text: #{text}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
|
3
|
+
module PDK
|
4
|
+
class TemplateFile < OpenStruct
|
5
|
+
# Initialises the TemplateFile object with the path to the template file
|
6
|
+
# and the data to be used when rendering the template.
|
7
|
+
#
|
8
|
+
# @param template_file [String] The path on disk to the template file.
|
9
|
+
# @param data [Hash{Symbol => Object}] The data that should be provided to
|
10
|
+
# the template when rendering.
|
11
|
+
# @option data [Object] :configs The value of this key will be provided to
|
12
|
+
# the template as an instance variable `@configs` in order to maintain
|
13
|
+
# compatibility with modulesync.
|
14
|
+
#
|
15
|
+
# @api public
|
16
|
+
def initialize(template_file, data = {})
|
17
|
+
@template_file = template_file
|
18
|
+
|
19
|
+
if data.has_key?(:configs)
|
20
|
+
@configs = data[:configs]
|
21
|
+
end
|
22
|
+
|
23
|
+
super(data)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Renders the template by calling the appropriate engine based on the file
|
27
|
+
# extension.
|
28
|
+
#
|
29
|
+
# If the template has an `.erb` extension, the content of the template
|
30
|
+
# file will be treated as an ERB template. All other extensions are treated
|
31
|
+
# as plain text.
|
32
|
+
#
|
33
|
+
# @return [String] The rendered template
|
34
|
+
#
|
35
|
+
# @raise (see #template_content)
|
36
|
+
#
|
37
|
+
# @api public
|
38
|
+
def render
|
39
|
+
case File.extname(@template_file)
|
40
|
+
when ".erb"
|
41
|
+
render_erb
|
42
|
+
else
|
43
|
+
render_plain
|
44
|
+
end
|
45
|
+
end
|
46
|
+
private
|
47
|
+
# Reads the content of the template file into memory.
|
48
|
+
#
|
49
|
+
# @return [String] The content of the template file.
|
50
|
+
#
|
51
|
+
# @raise [ArgumentError] If the template file does not exist or can not be
|
52
|
+
# read.
|
53
|
+
#
|
54
|
+
# @api private
|
55
|
+
def template_content
|
56
|
+
if File.file?(@template_file) && File.readable?(@template_file)
|
57
|
+
File.read(@template_file)
|
58
|
+
else
|
59
|
+
raise ArgumentError, _("'%{template}' is not a readable file") % {:template => @template_file}
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Renders the content of the template file as an ERB template.
|
64
|
+
#
|
65
|
+
# @return [String] The rendered template.
|
66
|
+
#
|
67
|
+
# @raise (see #template_content)
|
68
|
+
#
|
69
|
+
# @api private
|
70
|
+
def render_erb
|
71
|
+
renderer = ERB.new(template_content, nil, '-')
|
72
|
+
renderer.filename = @template_file
|
73
|
+
renderer.result(binding)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Renders the content of the template file as plain text.
|
77
|
+
#
|
78
|
+
# @return [String] The rendered template.
|
79
|
+
#
|
80
|
+
# @raise (see #template_content)
|
81
|
+
#
|
82
|
+
# @api private
|
83
|
+
def render_plain
|
84
|
+
template_content
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|