Skip to content

Commit

Permalink
Merge pull request #409 from DavidS/pdk-506-provider
Browse files Browse the repository at this point in the history
 (PDK-506) pdk new provider
  • Loading branch information
bmjen authored Jan 30, 2018
2 parents b939b8e + 68eba30 commit de93bf5
Show file tree
Hide file tree
Showing 8 changed files with 251 additions and 7 deletions.
1 change: 1 addition & 0 deletions lib/pdk/cli/new.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ module PDK::CLI
require 'pdk/cli/new/class'
require 'pdk/cli/new/defined_type'
require 'pdk/cli/new/module'
require 'pdk/cli/new/provider'
require 'pdk/cli/new/task'
27 changes: 27 additions & 0 deletions lib/pdk/cli/new/provider.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
module PDK::CLI
@new_provider_cmd = @new_cmd.define_command do
name 'provider'
usage _('provider [options] <name>')
summary _('[experimental] Create a new ruby provider named <name> using given options')

PDK::CLI.template_url_option(self)

run do |opts, args, _cmd|
PDK::CLI::Util.ensure_in_module!

provider_name = args[0]
module_dir = Dir.pwd

if provider_name.nil? || provider_name.empty?
puts command.help
exit 1
end

unless Util::OptionValidator.valid_provider_name?(provider_name)
raise PDK::CLI::ExitWithError, _("'%{name}' is not a valid provider name") % { name: provider_name }
end

PDK::Generate::Provider.new(module_dir, provider_name, opts).run
end
end
end
4 changes: 4 additions & 0 deletions lib/pdk/cli/util/option_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ def self.valid_module_name?(string)
end
singleton_class.send(:alias_method, :valid_task_name?, :valid_module_name?)

# https://puppet.com/docs/puppet/5.3/custom_types.html#creating-a-type only says the name has to be a ruby symbol.
# Let's assume that only strings similar to module names can actually be resolved by the puppet language.
singleton_class.send(:alias_method, :valid_provider_name?, :valid_module_name?)

# Validate a Puppet namespace against the regular expression in the
# documentation: https://docs.puppet.com/puppet/4.10/lang_reserved.html#classes-and-defined-resource-types
def self.valid_namespace?(string)
Expand Down
3 changes: 2 additions & 1 deletion lib/pdk/generate.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require 'pdk/generate/module'
require 'pdk/generate/defined_type'
require 'pdk/generate/module'
require 'pdk/generate/provider'
require 'pdk/generate/puppet_class'
require 'pdk/generate/task'
require 'pdk/module/metadata'
Expand Down
80 changes: 80 additions & 0 deletions lib/pdk/generate/provider.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
require 'pdk/generate/puppet_object'

module PDK
module Generate
class Provider < PuppetObject
OBJECT_TYPE = :provider

# Prepares the data needed to render the new defined type template.
#
# @return [Hash{Symbol => Object}] a hash of information that will be
# provided to the defined type and defined type spec templates during
# rendering.
def template_data
data = {
name: object_name,
provider_class: Provider.class_name_from_object_name(object_name),
}

data
end

def raise_precondition_error(error)
raise PDK::CLI::ExitWithError, _('%{error}: Creating a provider needs some local configuration in your module.' \
' Please follow the docs at https://github.com/puppetlabs/puppet-resource_api#getting-started.') % { error: error }
end

def check_preconditions
super
# These preconditions can be removed once the pdk-templates are carrying the puppet-resource_api gem by default, and have switched
# the default mock_with value.
sync_path = PDK::Util.find_upwards('.sync.yml')
if sync_path.nil?
raise_precondition_error(_('.sync.yml not found'))
end
sync = YAML.load_file(sync_path)
if !sync.is_a? Hash
raise_precondition_error(_('.sync.yml contents is not a Hash'))
elsif !sync.key? 'Gemfile'
raise_precondition_error(_('Gemfile configuration not found'))
elsif !sync['Gemfile'].key? 'optional'
raise_precondition_error(_('Gemfile.optional configuration not found'))
elsif !sync['Gemfile']['optional'].key? ':development'
raise_precondition_error(_('Gemfile.optional.:development configuration not found'))
elsif sync['Gemfile']['optional'][':development'].none? { |g| g['gem'] == 'puppet-resource_api' }
raise_precondition_error(_('puppet-resource_api not found in the Gemfile config'))
elsif !sync.key? 'spec/spec_helper.rb'
raise_precondition_error(_('spec/spec_helper.rb configuration not found'))
elsif !sync['spec/spec_helper.rb'].key? 'mock_with'
raise_precondition_error(_('spec/spec_helper.rb.mock_with configuration not found'))
elsif !sync['spec/spec_helper.rb']['mock_with'] == ':rspec'
raise_precondition_error(_('spec/spec_helper.rb.mock_with not set to \':rspec\''))
end
end

# @return [String] the path where the new type will be written.
def target_object_path
@target_object_path ||= File.join(module_dir, 'lib', 'puppet', 'type', object_name) + '.rb'
end

# @return [String] the path where the new provider will be written.
def target_addon_path
@target_addon_path ||= File.join(module_dir, 'lib', 'puppet', 'provider', object_name, object_name) + '.rb'
end

# Calculates the path to the file where the tests for the new defined
# type will be written.
#
# @return [String] the path where the tests for the new defined type
# will be written.
def target_spec_path
@target_spec_path ||= File.join(module_dir, 'spec', 'unit', 'puppet', 'provider', object_name, object_name) + '_spec.rb'
end

# transform a object name into a ruby class name
def self.class_name_from_object_name(object_name)
object_name.to_s.split('_').map(&:capitalize).join
end
end
end
end
31 changes: 25 additions & 6 deletions lib/pdk/generate/puppet_object.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ def target_object_path
raise NotImplementedError
end

# @abstract Subclass and implement {#target_addon_path}. Implementations
# of this method should return a String containing the destination path
# of the additional object file being generated.
# @return [String] returns nil if there is no additional object file
def target_addon_path
nil
end

# @abstract Subclass and implement {#target_spec_path}. Implementations
# of this method should return a String containing the destination path
# of the tests for the object being generated.
Expand All @@ -76,28 +84,39 @@ def object_type
self.class::OBJECT_TYPE
end

# Check that the target files do not exist, find an appropriate template
# and create the target files from the template. This is the main entry
# point for the class.
# Check preconditions of this template group. By default this only makes sure that the target files do not
# already exist. Override this (and call super) to add your own preconditions.
#
# @raise [PDK::CLI::ExitWithError] if the target files already exist.
# @raise [PDK::CLI::FatalError] (see #render_file)
#
# @api public
def run
[target_object_path, target_spec_path].compact.each do |target_file|
def check_preconditions
[target_object_path, target_addon_path, target_spec_path].compact.each do |target_file|
next unless File.exist?(target_file)

raise PDK::CLI::ExitWithError, _("Unable to generate %{object_type}; '%{file}' already exists.") % {
file: target_file,
object_type: object_type,
}
end
end

# Check that the templates can be rendered. Find an appropriate template
# and create the target files from the template. This is the main entry
# point for the class.
#
# @raise [PDK::CLI::ExitWithError] if the target files already exist.
# @raise [PDK::CLI::FatalError] (see #render_file)
#
# @api public
def run
check_preconditions

with_templates do |template_path, config_hash|
data = template_data.merge(configs: config_hash)

render_file(target_object_path, template_path[:object], data)
render_file(target_addon_path, template_path[:addon], data) if template_path[:addon]
render_file(target_spec_path, template_path[:spec], data) if template_path[:spec]
end
end
Expand Down
2 changes: 2 additions & 0 deletions lib/pdk/module/templatedir.rb
Original file line number Diff line number Diff line change
Expand Up @@ -153,10 +153,12 @@ def render
# @api public
def object_template_for(object_type)
object_path = File.join(@object_dir, "#{object_type}.erb")
addon_path = File.join(@object_dir, "#{object_type}_addon.erb")
spec_path = File.join(@object_dir, "#{object_type}_spec.erb")

if File.file?(object_path) && File.readable?(object_path)
result = { object: object_path }
result[:addon] = addon_path if File.file?(addon_path) && File.readable?(addon_path)
result[:spec] = spec_path if File.file?(spec_path) && File.readable?(spec_path)
result
else
Expand Down
110 changes: 110 additions & 0 deletions spec/acceptance/new_provider_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
require 'spec_helper_acceptance'

describe 'pdk new provider foo', module_command: true do
context 'in a new module' do
include_context 'in a new module', 'new_provider'

context 'when creating a provider' do
before(:all) do
File.open('.sync.yml', 'w') do |f|
f.write(<<SYNC)
---
Gemfile:
optional:
':development':
- gem: 'puppet-resource_api'
spec/spec_helper.rb:
mock_with: ':rspec'
SYNC
end
system('pdk convert --force')
end

describe command('pdk new provider test_provider') do
its(:stderr) { is_expected.to match(%r{creating .* from template}i) }
its(:stderr) { is_expected.not_to match(%r{WARN|ERR}) }
its(:stdout) { is_expected.to match(%r{\A\Z}) }
its(:exit_status) { is_expected.to eq(0) }
end

describe file('lib/puppet/type') do
it { is_expected.to be_directory }
end

describe file('lib/puppet/type/test_provider.rb') do
it { is_expected.to be_file }
its(:content) { is_expected.to match(%r{Puppet::ResourceApi.register_type}) }
its(:content) { is_expected.to match(%r{name: 'test_provider'}) }
end

describe file('lib/puppet/provider/test_provider') do
it { is_expected.to be_directory }
end

describe file('lib/puppet/provider/test_provider/test_provider.rb') do
it { is_expected.to be_file }
its(:content) { is_expected.to match(%r{class Puppet::Provider::TestProvider::TestProvider}) }
end

describe file('spec/unit/puppet/provider/test_provider') do
it { is_expected.to be_directory }
end

describe file('spec/unit/puppet/provider/test_provider/test_provider_spec.rb') do
it { is_expected.to be_file }
its(:content) { is_expected.to match(%r{RSpec.describe Puppet::Provider::TestProvider::TestProvider do}) }
end

context 'when validating the generated code' do
describe command('pdk validate ruby') do
its(:stdout) { is_expected.to be_empty }
its(:stderr) { is_expected.to be_empty }
its(:exit_status) { is_expected.to eq(0) }
end
end

context 'when running the generated spec tests' do
describe command('pdk test unit') do
its(:stderr) { is_expected.to match(%r{0 failures}) }
its(:stderr) { is_expected.not_to match(%r{no examples found}i) }
its(:exit_status) { is_expected.to eq(0) }
end
end

context 'without a .sync.yml' do
before(:all) do
FileUtils.mv('.sync.yml', 'sync.yml.orig')
end

describe command('pdk new provider test_provider2') do
its(:stderr) { is_expected.to match(%r{pdk \(ERROR\): .sync.yml not found}i) }
its(:stdout) { is_expected.to match(%r{\A\Z}) }
its(:exit_status) { is_expected.not_to eq(0) }
end
end

context 'with invalid .sync.yml' do
before(:all) do
File.open('.sync.yml', 'w') do |f|
f.write(<<SYNC)
---
Gemfile:
optional:
':wrong_group':
- gem: 'puppet-resource_api'
spec/spec_helper.rb:
mock_with: ':rspec'
SYNC
end
system('pdk convert --force')
end

describe command('pdk new provider test_provider2') do
its(:stderr) { is_expected.to match(%r{pdk \(ERROR\): Gemfile.optional.:development configuration not found}i) }
its(:stdout) { is_expected.to match(%r{\A\Z}) }
its(:exit_status) { is_expected.not_to eq(0) }
end
end
end
end
end

0 comments on commit de93bf5

Please sign in to comment.