Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(PDK-506) pdk new provider #409

Merged
merged 2 commits into from
Jan 30, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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