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.
@@ -0,0 +1,4 @@
1
+ require 'gettext-setup'
2
+
3
+ GettextSetup.initialize(File.absolute_path('../../locales', File.dirname(__FILE__)))
4
+ GettextSetup.negotiate_locale!(GettextSetup.candidate_locales)
@@ -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
@@ -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