Skip to content

Commit

Permalink
(FM-8081) Implement pdk new transport
Browse files Browse the repository at this point in the history
This change implements a `pdk new transport` command for
using the new Resource API transports. It is closely
modelled on the `pdk new provider` command.
  • Loading branch information
DavidS committed Jun 17, 2019
1 parent e98a7c2 commit 8b11d9c
Show file tree
Hide file tree
Showing 9 changed files with 265 additions and 6 deletions.
1 change: 1 addition & 0 deletions lib/pdk/cli/new.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ module PDK::CLI
require 'pdk/cli/new/module'
require 'pdk/cli/new/provider'
require 'pdk/cli/new/task'
require 'pdk/cli/new/transport'
25 changes: 25 additions & 0 deletions lib/pdk/cli/new/transport.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
module PDK::CLI
@new_transport_cmd = @new_cmd.define_command do
name 'transport'
usage _('transport [options] <name>')
summary _('[experimental] Create a new ruby transport named <name> using given options')

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

transport_name = args[0]
module_dir = Dir.pwd

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

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

PDK::Generate::Transport.new(module_dir, transport_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 @@ -28,6 +28,10 @@ def self.valid_module_name?(string)
# 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?)

# The name has to be a ruby symbol.
# While overly strict, let's apply the provider and module name rules for consistency.
singleton_class.send(:alias_method, :valid_transport_name?, :valid_provider_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
1 change: 1 addition & 0 deletions lib/pdk/generate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require 'pdk/generate/provider'
require 'pdk/generate/puppet_class'
require 'pdk/generate/task'
require 'pdk/generate/transport'
require 'pdk/module/metadata'
require 'pdk/module/templatedir'

Expand Down
5 changes: 0 additions & 5 deletions lib/pdk/generate/provider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,6 @@ def target_spec_path
def target_type_spec_path
@target_type_spec_path ||= File.join(module_dir, 'spec', 'unit', 'puppet', 'type', 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
15 changes: 14 additions & 1 deletion lib/pdk/generate/puppet_object.rb
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,13 @@ def target_type_spec_path
nil
end

# @abstract Subclass and implement {#target_device_path}. Implementations
# of this method should return a String containing the destination path
# of the device class being generated.
def target_device_path
nil
end

# Retrieves the type of the object being generated, e.g. :class,
# :defined_type, etc. This is specified in the subclass' OBJECT_TYPE
# constant.
Expand All @@ -99,7 +106,7 @@ def object_type
#
# @api public
def check_preconditions
[target_object_path, target_type_path, target_spec_path, target_type_spec_path].compact.each do |target_file|
[target_object_path, target_type_path, target_device_path, target_spec_path, target_type_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 @@ -125,6 +132,7 @@ def run

render_file(target_object_path, template_path[:object], data)
render_file(target_type_path, template_path[:type], data) if template_path[:type]
render_file(target_device_path, template_path[:device], data) if template_path[:device]
render_file(target_spec_path, template_path[:spec], data) if template_path[:spec]
render_file(target_type_spec_path, template_path[:type_spec], data) if template_path[:type_spec]
end
Expand Down Expand Up @@ -279,6 +287,11 @@ def module_metadata
raise PDK::CLI::FatalError, _("'%{dir}' does not contain valid Puppet module metadata: %{msg}") % { dir: module_dir, msg: e.message }
end
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
87 changes: 87 additions & 0 deletions lib/pdk/generate/transport.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
require 'pdk/generate/puppet_object'

module PDK
module Generate
class Transport < PuppetObject
OBJECT_TYPE = :transport

# 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,
transport_class: Transport.class_name_from_object_name(object_name),
}

data
end

def raise_precondition_error(error)
raise PDK::CLI::ExitWithError, _('%{error}: Creating a transport 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 transport will be written.
def target_object_path
@target_object_path ||= File.join(module_dir, 'lib', 'puppet', 'transport', object_name) + '.rb'
end

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

# @return [String] the path where the deviceshim for the transport will be written.
def target_device_path
@target_device_path ||= File.join(module_dir, 'lib', 'puppet', 'util', 'network_device', object_name, 'device.rb')
end

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

# @return [String] the path where the tests for the new schema will be written.
def target_type_spec_path
@target_type_spec_path ||= File.join(module_dir, 'spec', 'unit', 'puppet', 'transport', 'schema', 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
2 changes: 2 additions & 0 deletions lib/pdk/module/templatedir.rb
Original file line number Diff line number Diff line change
Expand Up @@ -160,13 +160,15 @@ def render
def object_template_for(object_type)
object_path = File.join(@object_dir, "#{object_type}.erb")
type_path = File.join(@object_dir, "#{object_type}_type.erb")
device_path = File.join(@object_dir, "#{object_type}_device.erb")
spec_path = File.join(@object_dir, "#{object_type}_spec.erb")
type_spec_path = File.join(@object_dir, "#{object_type}_type_spec.erb")

if File.file?(object_path) && File.readable?(object_path)
result = { object: object_path }
result[:type] = type_path if File.file?(type_path) && File.readable?(type_path)
result[:spec] = spec_path if File.file?(spec_path) && File.readable?(spec_path)
result[:device] = device_path if File.file?(device_path) && File.readable?(device_path)
result[:type_spec] = type_spec_path if File.file?(type_spec_path) && File.readable?(type_spec_path)
result
else
Expand Down
131 changes: 131 additions & 0 deletions spec/acceptance/new_transport_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
require 'spec_helper_acceptance'

describe 'pdk new transport', module_command: true do
def update_module!
command('pdk update --force').exit_status
end

context 'when run inside of a module' do
include_context 'in a new module', 'new_transport'

context 'with the Resource API configured in .sync.yml' 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
update_module!
end

describe command('pdk new transport test_transport') 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 have_no_output }
its(:exit_status) { is_expected.to eq(0) }

describe file(File.join('lib', 'puppet', 'transport')) do
it { is_expected.to be_directory }
end

describe file(File.join('lib', 'puppet', 'transport', 'test_transport.rb')) do
it { is_expected.to be_file }
its(:content) { is_expected.to match(%r{class TestTransport}) }
end

describe file(File.join('lib', 'puppet', 'transport', 'schema')) do
it { is_expected.to be_directory }
end

describe file(File.join('lib', 'puppet', 'transport', 'schema', 'test_transport.rb')) do
it { is_expected.to be_file }
its(:content) { is_expected.to match(%r{Puppet::ResourceApi.register_transport}) }
its(:content) { is_expected.to match(%r{name: 'test_transport'}) }
end

describe file(File.join('lib', 'puppet', 'util', 'network_device', 'test_transport')) do
it { is_expected.to be_directory }
end

describe file(File.join('lib', 'puppet', 'util', 'network_device', 'test_transport', 'device.rb')) do
it { is_expected.to be_file }
its(:content) { is_expected.to match(%r{Puppet::Util::NetworkDevice::Test_transport}) } # puppet defaults to `capitalize`ing instead of snake case like ruby would prefer
its(:content) { is_expected.to match(%r{super\('test_transport', url_or_config\)}) }
end

describe file(File.join('spec', 'unit', 'puppet', 'transport')) do
it { is_expected.to be_directory }
end

describe file(File.join('spec', 'unit', 'puppet', 'transport', 'test_transport_spec.rb')) do
it { is_expected.to be_file }
its(:content) { is_expected.to match(%r{RSpec.describe Puppet::Transport::TestTransport do}) }
end

describe file(File.join('spec', 'unit', 'puppet', 'transport', 'schema')) do
it { is_expected.to be_directory }
end

describe file(File.join('spec', 'unit', 'puppet', 'transport', 'schema', 'test_transport_spec.rb')) do
it { is_expected.to be_file }
its(:content) { is_expected.to match(%r{RSpec.describe 'the test_transport transport' do}) }
end

describe command('pdk validate ruby') do
its(:stdout) { is_expected.to have_no_output }
its(:stderr) { is_expected.to match(%r{using ruby \d+\.\d+\.\d+}i) }
its(:stderr) { is_expected.to match(%r{using puppet \d+\.\d+\.\d+}i) }
its(:exit_status) { is_expected.to eq(0) }
end

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

context 'without a .sync.yml' do
before(:all) do
FileUtils.rm_f('.sync.yml')
update_module!
end

describe command('pdk new transport test_transport2') do
its(:stderr) { is_expected.to match(%r{pdk \(ERROR\): .sync.yml not found}i) }
its(:stdout) { is_expected.to have_no_output }
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:
':test':
- gem: 'puppet-resource_api'
spec/spec_helper.rb:
mock_with: ':rspec'
SYNC
end
update_module!
end

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

0 comments on commit 8b11d9c

Please sign in to comment.