pdk 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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