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,74 @@
1
+ module PDK
2
+ module CLI
3
+ module Util
4
+ class OptionValidator
5
+ def self.is_comma_separated_list?(list, options = {})
6
+ list =~ /^[\w\-]+(?:,[\w\-]+)+$/ ? true : false
7
+ end
8
+
9
+ def self.enum(val, valid_entries, options = {})
10
+ vals = val.is_a?(Array) ? val : [val]
11
+ invalid_entries = vals.find_all { |e| !valid_entries.include?(e) }
12
+
13
+ unless invalid_entries.empty?
14
+ raise _("Error: the following values are invalid: %{invalid_entries}") % {invalid_entries: invalid_entries}
15
+ end
16
+
17
+ val
18
+ end
19
+
20
+ # Validate the module name against the regular expression in the
21
+ # documentation: https://docs.puppet.com/puppet/4.10/modules_fundamentals.html#allowed-module-names
22
+ def self.is_valid_module_name?(string)
23
+ !(string =~ /\A[a-z][a-z0-9_]*\Z/).nil?
24
+ end
25
+
26
+ # Validate a Puppet namespace against the regular expression in the
27
+ # documentation: https://docs.puppet.com/puppet/4.10/lang_reserved.html#classes-and-defined-resource-types
28
+ def self.is_valid_namespace?(string)
29
+ return false if (string || '').split('::').last == 'init'
30
+
31
+ !(string =~ /\A([a-z][a-z0-9_]*)(::[a-z][a-z0-9_]*)*\Z/).nil?
32
+ end
33
+
34
+ singleton_class.send(:alias_method, :is_valid_class_name?, :is_valid_namespace?)
35
+ singleton_class.send(:alias_method, :is_valid_defined_type_name?, :is_valid_namespace?)
36
+
37
+ # Validate that a class/defined type parameter matches the regular
38
+ # expression in the documentation: https://docs.puppet.com/puppet/4.10/lang_reserved.html#parameters
39
+ # The parameter should also not be a reserved word or overload
40
+ # a metaparameter.
41
+ def self.is_valid_param_name?(string)
42
+ reserved_words = %w{trusted facts server_facts title name}.freeze
43
+ metaparams = %w{alias audit before loglevel noop notify require schedule stage subscribe tag}.freeze
44
+ return false if reserved_words.include?(string) || metaparams.include?(string)
45
+
46
+ !(string =~ /\A[a-z][a-zA-Z0-9_]*\Z/).nil?
47
+ end
48
+
49
+ # Naive validation of a data type declaration. Extracts all the bare
50
+ # words and compares them against a list of known data types.
51
+ #
52
+ # @todo This prevents the use of dynamic data types like TypeReferences
53
+ # but that shouldn't be a problem for the current feature set. This
54
+ # should be replaced eventually by something better (or just call
55
+ # Puppet::Pops::Types::TypesParser)
56
+ def self.is_valid_data_type?(string)
57
+ valid_types = %w{
58
+ String Integer Float Numeric Boolean Array Hash Regexp Undef
59
+ Default Class Resource Scalar Collection Variant Data Pattern Enum
60
+ Tuple Struct Optional Catalogentry Type Any Callable NotUndef
61
+ }.freeze
62
+
63
+ string.scan(/\b(([a-zA-Z]+)(,|\[|\]|\Z))/) do |result|
64
+ type = result[1]
65
+
66
+ return false unless valid_types.include?(type)
67
+ end
68
+
69
+ true
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,81 @@
1
+ require 'cri'
2
+ require 'pdk/cli/util/option_validator'
3
+ require 'pdk/report'
4
+
5
+ require 'pdk/validate'
6
+
7
+ module PDK
8
+ module CLI
9
+ class Validate
10
+ include PDK::CLI::Util
11
+
12
+ def self.command
13
+ @validate ||= Cri::Command.define do
14
+ name 'validate'
15
+ usage _("validate [options]")
16
+ summary _("Run static analysis tests.")
17
+ description _("Run metadata-json-lint, puppet parser validate, puppet-lint, or rubocop.")
18
+
19
+ flag nil, :list, _("list all available validators")
20
+
21
+ run do |opts, args, cmd|
22
+ validator_names = PDK::Validate.validators.map { |v| v.name }
23
+ validators = PDK::Validate.validators
24
+ targets = []
25
+ reports = nil
26
+
27
+ if opts[:list]
28
+ puts _("Available validators: %{validator_names}") % {validator_names: validator_names.join(', ')}
29
+ exit 0
30
+ end
31
+
32
+ if args[0]
33
+ # This may be a single validator, a list of validators, or a target.
34
+ if OptionValidator.is_comma_separated_list?(args[0])
35
+ # This is a comma separated list. Treat each item as a validator.
36
+
37
+ vals = OptionNormalizer.comma_separated_list_to_array(args[0])
38
+ validators = PDK::Validate.validators.find_all { |v| vals.include?(v.name) }
39
+
40
+ invalid = vals.find_all { |v| !validator_names.include?(v) }
41
+ invalid.each do |v|
42
+ PDK.logger.warn(_("Unknown validator '%{v}'. Available validators: %{validators}") % {v: v, validators: validator_names.join(', ')})
43
+ end
44
+ else
45
+ # This is a single item. Check if it's a known validator, or otherwise treat it as a target.
46
+ val = PDK::Validate.validators.find { |v| args[0] == v.name }
47
+ if val
48
+ validators = [val]
49
+ else
50
+ targets = [args[0]]
51
+ # We now know that no validators were passed, so let the user know we're using all of them by default.
52
+ PDK.logger.info(_("Running all available validators..."))
53
+ end
54
+ end
55
+ else
56
+ PDK.logger.info(_("Running all available validators..."))
57
+ end
58
+
59
+ # Subsequent arguments are targets.
60
+ targets.concat(args[1..-1]) if args.length > 1
61
+
62
+ # Note: Reporting may be delegated to the validation tool itself.
63
+ if opts[:format]
64
+ reports = OptionNormalizer.report_formats(opts.fetch(:format))
65
+ end
66
+
67
+ options = targets.empty? ? {} : { :targets => targets }
68
+ validators.each do |validator|
69
+ result = validator.invoke(options)
70
+ if reports
71
+ reports.each do |r|
72
+ r.write(result)
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,3 @@
1
+ module PDK
2
+ module Generate; end
3
+ end
@@ -0,0 +1,139 @@
1
+ require 'etc'
2
+ require 'pathname'
3
+ require 'fileutils'
4
+
5
+ require 'pdk'
6
+ require 'pdk/logger'
7
+ require 'pdk/module/metadata'
8
+ require 'pdk/module/templatedir'
9
+ require 'pdk/cli/exec'
10
+ require 'pdk/cli/input'
11
+ require 'pdk/util'
12
+
13
+ module PDK
14
+ module Generate
15
+ class Module
16
+ DEFAULT_TEMPLATE = 'https://github.com/puppetlabs/pdk-module-template'
17
+
18
+ def self.invoke(opts={})
19
+ defaults = {
20
+ 'version' => '0.1.0',
21
+ 'dependencies' => [
22
+ { 'name' => 'puppetlabs-stdlib', 'version_requirement' => '>= 1.0.0' }
23
+ ]
24
+ }
25
+
26
+ defaults['license'] = opts[:license] if opts.has_key? :license
27
+ target_dir = File.expand_path(opts[:target_dir])
28
+
29
+ if File.exists?(target_dir)
30
+ raise PDK::CLI::FatalError, _("The destination directory '%{dir}' already exists") % {:dir => target_dir}
31
+ end
32
+
33
+ metadata = PDK::Module::Metadata.new(defaults)
34
+
35
+ module_interview(metadata, opts) unless opts[:'skip-interview'] # @todo Build way to get info by answers file
36
+
37
+ temp_target_dir = PDK::Util.make_tmpdir_name('pdk-module-target')
38
+
39
+ prepare_module_directory(temp_target_dir)
40
+
41
+ template_url = opts.fetch(:'template-url', DEFAULT_TEMPLATE)
42
+
43
+ PDK::Module::TemplateDir.new(template_url) do |templates|
44
+ templates.render do |file_path, file_content|
45
+ file = Pathname.new(temp_target_dir) + file_path
46
+ file.dirname.mkpath
47
+ file.write(file_content)
48
+ end
49
+
50
+ # Add information about the template used to generate the module to the
51
+ # metadata (for a future update command).
52
+ metadata.update!(templates.metadata)
53
+
54
+ File.open(File.join(temp_target_dir, 'metadata.json'), 'w') do |metadata_file|
55
+ metadata_file.puts metadata.to_json
56
+ end
57
+ end
58
+
59
+ FileUtils.mv(temp_target_dir, target_dir)
60
+ end
61
+
62
+ def self.prepare_module_directory(target_dir)
63
+ [
64
+ File.join(target_dir, 'manifests'),
65
+ File.join(target_dir, 'templates'),
66
+ ].each do |dir|
67
+ begin
68
+ FileUtils.mkdir_p(dir)
69
+ rescue SystemCallError
70
+ raise PDK::CLI::FatalError, _("Unable to create directory '%{dir}'") % {:dir => dir}
71
+ end
72
+ end
73
+ end
74
+
75
+ def self.module_interview(metadata, opts={})
76
+ puts _(
77
+ "We need to create a metadata.json file for this module. Please answer the " +
78
+ "following questions; if the question is not applicable to this module, feel free " +
79
+ "to leave it blank."
80
+ )
81
+
82
+ begin
83
+ puts ""
84
+ forge_user = PDK::CLI::Input.get(_("What is your Puppet Forge username?"), Etc.getlogin)
85
+ metadata.update!('name' => "#{forge_user}-#{opts[:name]}")
86
+ rescue StandardError => e
87
+ PDK.logger.error(_("We're sorry, we could not parse your module name: %{message}") % {:message => e.message})
88
+ retry
89
+ end
90
+
91
+ begin
92
+ puts "\n" + _("Puppet uses Semantic Versioning (semver.org) to version modules.")
93
+ module_version = PDK::CLI::Input.get(_("What version is this module?"), metadata.data['version'])
94
+ metadata.update!('version' => module_version)
95
+ rescue StandardError => e
96
+ PDK.logger.error(_("We're sorry, we could not parse that as a Semantic Version: %{message}") % {message: e.message})
97
+ retry
98
+ end
99
+
100
+ puts ""
101
+ module_author = PDK::CLI::Input.get(_("Who wrote this module?"), metadata.data['author'])
102
+ metadata.update!('author' => module_author)
103
+
104
+ unless opts.has_key?(:license)
105
+ puts ""
106
+ module_license = PDK::CLI::Input.get(_("What license does this module code fall under?"), metadata.data['license'])
107
+ metadata.update!('license' => module_license)
108
+ end
109
+
110
+ puts ""
111
+ module_summary = PDK::CLI::Input.get(_("How would you describe this module in a single sentence?"))
112
+ metadata.update!('summary' => module_summary)
113
+
114
+ puts ""
115
+ module_source = PDK::CLI::Input.get(_("Where is this module's source code repository?"))
116
+ metadata.update!('source' => module_source)
117
+
118
+ puts ""
119
+ module_page = PDK::CLI::Input.get(_("Where can others go to learn more about this module?"), metadata.data['project_page'])
120
+ metadata.update!('project_page' => module_page)
121
+
122
+ puts ""
123
+ module_issues = PDK::CLI::Input.get(_("Where can others go to file issues about this module?"), metadata.data['issues_url'])
124
+ metadata.update!('issues_url' => module_issues)
125
+
126
+ puts
127
+ puts '-' * 40
128
+ puts metadata.to_json
129
+ puts '-' * 40
130
+ puts
131
+
132
+ if PDK::CLI::Input.get(_("About to generate this module; continue?"), 'Y') !~ /^y(es)?$/i
133
+ puts _("Aborting...")
134
+ exit 0
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,51 @@
1
+ require 'pdk/generators/puppet_object'
2
+
3
+ module PDK
4
+ module Generate
5
+ class PuppetClass < PuppetObject
6
+ OBJECT_TYPE = :class
7
+
8
+ # Prepares the data needed to render the new Puppet class template.
9
+ #
10
+ # @return [Hash{Symbol => Object}] a hash of information that will be
11
+ # provided to the class and class spec templates during rendering.
12
+ def template_data
13
+ data = {name: object_name}
14
+ if @options.key?(:params)
15
+ data[:params] = @options[:params]
16
+ data[:max_type_length] = @options[:params].map { |r| r[:type].length }.max
17
+ end
18
+ data
19
+ end
20
+
21
+ # Calculates the path to the .pp file that the new class will be written
22
+ # to.
23
+ #
24
+ # @return [String] the path where the new class will be written.
25
+ def target_object_path
26
+ @target_pp_path ||= begin
27
+ class_name_parts = object_name.split('::')[1..-1]
28
+ class_name_parts << 'init' if class_name_parts.empty?
29
+
30
+ "#{File.join(module_dir, 'manifests', *class_name_parts)}.pp"
31
+ end
32
+ end
33
+
34
+ # Calculates the path to the file that the tests for the new class will
35
+ # be written to.
36
+ #
37
+ # @return [String] the path where the tests for the new class will be
38
+ # written.
39
+ def target_spec_path
40
+ @target_spec_path ||= begin
41
+ class_name_parts = object_name.split('::')
42
+
43
+ # drop the module name if the object name contains multiple parts
44
+ class_name_parts.delete_at(0) if class_name_parts.length > 1
45
+
46
+ "#{File.join(module_dir, 'spec', 'classes', *class_name_parts)}_spec.rb"
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,213 @@
1
+ require 'fileutils'
2
+
3
+ require 'pdk'
4
+ require 'pdk/logger'
5
+ require 'pdk/module/metadata'
6
+ require 'pdk/module/templatedir'
7
+ require 'pdk/template_file'
8
+
9
+ module PDK
10
+ module Generate
11
+ class PuppetObject
12
+ attr_reader :module_dir
13
+ attr_reader :object_name
14
+
15
+ # Initialises the PDK::Generate::PuppetObject object.
16
+ #
17
+ # In general, this object should never be instantiated directly. Instead,
18
+ # one of the subclasses should be used e.g. PDK::Generate::Klass.
19
+ #
20
+ # New subclasses generally only need to inherit this class, set the
21
+ # OBJECT_TYPE constant and implement the {#template_data},
22
+ # {#target_object_path} and {#target_spec_path} methods.
23
+ #
24
+ # @param module_dir [String] The path to the module directory that the
25
+ # will contain the object.
26
+ # @param object_name [String] The name of the object.
27
+ # @param options [Hash{Symbol => Object}]
28
+ #
29
+ # @api public
30
+ def initialize(module_dir, object_name, options = {})
31
+ @module_dir = module_dir
32
+ @options = options
33
+
34
+ if [:class, :defined_type].include?(object_type)
35
+ object_name_parts = object_name.split('::')
36
+
37
+ if object_name_parts.first == module_name
38
+ @object_name = object_name
39
+ else
40
+ @object_name = [module_name, object_name].join('::')
41
+ end
42
+ end
43
+ end
44
+
45
+ # @abstract Subclass and implement {#template_data} to provide data to
46
+ # the templates during rendering. Implementations of this method should
47
+ # return a Hash[{Symbol => Object}].
48
+ def template_data
49
+ raise NotImplementedError
50
+ end
51
+
52
+ # @abstract Subclass and implement {#target_object_path}. Implementations
53
+ # of this method should return a String containing the destination path
54
+ # of the object being generated.
55
+ def target_object_path
56
+ raise NotImplementedError
57
+ end
58
+
59
+ # @abstract Subclass and implement {#target_spec_path}. Implementations
60
+ # of this method should return a String containing the destination path
61
+ # of the tests for the object being generated.
62
+ def target_spec_path
63
+ raise NotImplementedError
64
+ end
65
+
66
+ # Retrieves the type of the object being generated, e.g. :class,
67
+ # :defined_type, etc. This is specified in the subclass' OBJECT_TYPE
68
+ # constant.
69
+ #
70
+ # @return [Symbol] the type of the object being generated.
71
+ #
72
+ # @api private
73
+ def object_type
74
+ self.class::OBJECT_TYPE
75
+ end
76
+
77
+ # Check that the target files do not exist, find an appropriate template
78
+ # and create the target files from the template. This is the main entry
79
+ # point for the class.
80
+ #
81
+ # @raise [PDK::CLI::FatalError] if the target files already exist.
82
+ #
83
+ # @api public
84
+ def run
85
+ [target_object_path, target_spec_path].each do |target_file|
86
+ if File.exist?(target_file)
87
+ raise PDK::CLI::FatalError, _("Unable to generate class, '%{file}' already exists.") % {file: target_file}
88
+ end
89
+ end
90
+
91
+ with_templates do |template_path, config_hash|
92
+ data = template_data.merge(:configs => config_hash)
93
+
94
+ render_file(target_object_path, template_path[:object], data)
95
+ render_file(target_spec_path, template_path[:spec], data) if template_path[:spec]
96
+ end
97
+ end
98
+
99
+ # Render a file using the provided template and write it to disk.
100
+ #
101
+ # @param dest_path [String] The path that the rendered file should be
102
+ # written to. Any necessary directories will be automatically created.
103
+ # @param template_path [String] The path on disk to the file containing
104
+ # the template.
105
+ # @param data [Hash{Object => Object}] The data to be provided to the
106
+ # template when rendering.
107
+ #
108
+ # @return [void]
109
+ #
110
+ # @api private
111
+ def render_file(dest_path, template_path, data)
112
+ PDK.logger.info(_("Creating %{file} from template.") % {file: dest_path})
113
+ file_content = PDK::TemplateFile.new(template_path, data).render
114
+ FileUtils.mkdir_p(File.dirname(dest_path))
115
+ File.open(dest_path, 'w') { |f| f.write file_content }
116
+ end
117
+
118
+ # Search the possible template directories in order of preference to find
119
+ # a template that can be used to render a new object of the specified
120
+ # type.
121
+ #
122
+ # @yieldparam template_paths [Hash{Symbol => String}] :object contains
123
+ # the path on disk to the template file for the object, :spec contains
124
+ # the path on disk to the template file for the tests for the object
125
+ # (if it exists).
126
+ # @yieldparam config_hash [Hash{Object => Object}] the contents of the
127
+ # :global key in the config_defaults.yml file.
128
+ #
129
+ # @raise [PDK::CLI::FatalError] if no suitable template could be found.
130
+ #
131
+ # @api private
132
+ def with_templates
133
+ templates.each do |template|
134
+ if template[:url].nil?
135
+ PDK.logger.debug(_("No %{dir_type} template specified; trying next template directory.") % {dir_type: template[:type]})
136
+ next
137
+ end
138
+
139
+ PDK::Module::TemplateDir.new(template[:url]) do |template_dir|
140
+ template_paths = template_dir.object_template_for(object_type)
141
+
142
+ if template_paths
143
+ config_hash = template_dir.object_config
144
+ yield template_paths, config_hash
145
+ return
146
+ else
147
+ if template[:allow_fallback]
148
+ PDK.logger.debug(_("Unable to find a %{type} template in %{url}, trying next template directory") % {type: object_type, url: template[:url]})
149
+ else
150
+ raise PDK::CLI::FatalError, _("Unable to find the %{type} template in %{url}.") % {type: object_type, url: template[:url]}
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
156
+
157
+ # Provides the possible template directory locations in the order in
158
+ # which they should be searched for a valid template.
159
+ #
160
+ # If a template-url has been specified on in the options hash (e.g. from
161
+ # a CLI parameter), then this template directory will be checked first
162
+ # and we do not fall back to the next possible template directory.
163
+ #
164
+ # If we have not been provided a specific template directory to use, we
165
+ # try the template specified in the module metadata (as set during
166
+ # PDK::Generate::Module) and fall back to the default template if
167
+ # necessary.
168
+ #
169
+ # @return [Array<Hash{Symbol => Object}>] an array of hashes. Each hash
170
+ # contains 3 keys: :type contains a String that describes the template
171
+ # directory, :url contains a String with the URL to the template
172
+ # directory, and :allow_fallback contains a Boolean that specifies if
173
+ # the lookup process should proceed to the next template directory if
174
+ # the template file is not in this template directory.
175
+ #
176
+ # @api private
177
+ def templates
178
+ @templates ||= [
179
+ {type: 'CLI', url: @options[:'template-url'], allow_fallback: false},
180
+ {type: 'metadata', url: module_metadata.data['template-url'], allow_fallback: true},
181
+ {type: 'default', url: PDK::Generate::Module::DEFAULT_TEMPLATE, allow_fallback: false},
182
+ ]
183
+ end
184
+
185
+ # Retrieves the name of the module (without the forge username) from the
186
+ # module metadata.
187
+ #
188
+ # @raise (see #module_metadata)
189
+ # @return [String] The name of the module.
190
+ #
191
+ # @api private
192
+ def module_name
193
+ @module_name ||= module_metadata.data['name'].rpartition('-').last
194
+ end
195
+
196
+ # Parses the metadata.json file for the module.
197
+ #
198
+ # @raise [PDK::CLI::FatalError] if the metadata.json file does not exist,
199
+ # can not be read, or contains invalid metadata.
200
+ #
201
+ # @return [PDK::Module::Metadata] the parsed module metadata.
202
+ #
203
+ # @api private
204
+ def module_metadata
205
+ @module_metadata ||= begin
206
+ PDK::Module::Metadata.from_file(File.join(module_dir, 'metadata.json'))
207
+ rescue ArgumentError => e
208
+ raise PDK::CLI::FatalError, _("'%{dir}' does not contain valid Puppet module metadata; %{msg}") % {dir: module_dir, msg: e.message}
209
+ end
210
+ end
211
+ end
212
+ end
213
+ end