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,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