Skip to content

Commit

Permalink
(PDK-506) pdk new provider
Browse files Browse the repository at this point in the history
This implements `pdk new provider` command to support use of the Resource
API in modules.

This requires the template changes from puppetlabs/pdk-templates#13

See the README on https://github.com/puppetlabs/puppet-resource_api for details
  • Loading branch information
DavidS committed Jan 22, 2018
1 parent 2b55f1a commit ce58c5e
Show file tree
Hide file tree
Showing 8 changed files with 174 additions and 2 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'
30 changes: 30 additions & 0 deletions lib/pdk/cli/new/provider.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
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!(
message: _('Providers can only be created from inside a valid module directory.'),
log_level: :info,
)

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: defined_type_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
47 changes: 47 additions & 0 deletions lib/pdk/generate/provider.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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

# @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
11 changes: 10 additions & 1 deletion 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
return 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 Down Expand Up @@ -85,7 +93,7 @@ def object_type
#
# @api public
def run
[target_object_path, target_spec_path].compact.each do |target_file|
[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.") % {
Expand All @@ -98,6 +106,7 @@ def run
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
78 changes: 78 additions & 0 deletions spec/acceptance/new_provider_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
require 'spec_helper_acceptance'

describe 'pdk new provider', module_command: true do
shared_examples 'it creates a provider' do |name|
describe command("pdk new provider #{name}") 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/provider/#{name}") do
it { is_expected.to be_directory }
end

describe file("spec/unit/puppet/provider/#{name}") do
it { is_expected.to be_directory }
end

context 'with the experimental `puppet-resource_api` in the Gemfile' do
describe command('echo "gem \'puppet-resource_api\', git: \'https://github.com/puppetlabs/puppet-resource_api.git\'" >> Gemfile') do
its(:exit_status) { is_expected.to eq(0) }
end

describe command('echo "RSpec.configure { |c| c.mock_with :rspec }" >> spec/spec_helper.rb') do
its(:exit_status) { is_expected.to eq(0) }
end

describe command('rm -f Gemfile.lock') do
its(:exit_status) { is_expected.to eq(0) }
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
end
end

context 'in a fresh module' do
include_context 'in a new module', 'new_provider'

context 'when creating a provider' do
it_behaves_like 'it creates a provider', 'test_provider'

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/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/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
end
end
end

0 comments on commit ce58c5e

Please sign in to comment.