summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--lib/puppet/dsl.rb4
-rw-r--r--lib/puppet/dsl/resource_type_api.rb48
-rw-r--r--lib/puppet/parser/ast/definition.rb7
-rw-r--r--lib/puppet/parser/ast/hostclass.rb7
-rw-r--r--lib/puppet/parser/ast/node.rb7
-rw-r--r--lib/puppet/parser/parser_support.rb11
-rw-r--r--spec/integration/parser/ruby_manifest_spec.rb126
-rwxr-xr-xspec/unit/dsl/resource_type_api_spec.rb36
-rwxr-xr-xspec/unit/parser/parser_spec.rb9
9 files changed, 193 insertions, 62 deletions
diff --git a/lib/puppet/dsl.rb b/lib/puppet/dsl.rb
index abdb78f67..97a310436 100644
--- a/lib/puppet/dsl.rb
+++ b/lib/puppet/dsl.rb
@@ -5,7 +5,3 @@ end
require 'puppet/dsl/resource_type_api'
require 'puppet/dsl/resource_api'
-
-class Object
- include Puppet::DSL::ResourceTypeAPI
-end
diff --git a/lib/puppet/dsl/resource_type_api.rb b/lib/puppet/dsl/resource_type_api.rb
index 487aab99d..8810d5368 100644
--- a/lib/puppet/dsl/resource_type_api.rb
+++ b/lib/puppet/dsl/resource_type_api.rb
@@ -1,34 +1,15 @@
require 'puppet/resource/type'
-module Puppet::DSL::ResourceTypeAPI
- def define(name, *args, &block)
- result = mk_resource_type(:definition, name, Hash.new, block)
- result.set_arguments(munge_type_arguments(args))
- result
- end
-
- def hostclass(name, options = {}, &block)
- mk_resource_type(:hostclass, name, options, block)
- end
-
- def node(name, options = {}, &block)
- mk_resource_type(:node, name, options, block)
- end
-
- private
-
- def mk_resource_type(type, name, options, code)
- klass = Puppet::Resource::Type.new(type, name, options)
-
- klass.ruby_code = code if code
-
- Puppet::Node::Environment.new.known_resource_types.add klass
-
- klass
+# Type of the objects inside of which pure ruby manifest files are
+# executed. Provides methods for creating defines, hostclasses, and
+# nodes.
+class Puppet::DSL::ResourceTypeAPI
+ def initialize
+ @__created_ast_objects__ = []
end
- def munge_type_arguments(args)
- args.inject([]) do |result, item|
+ def define(name, *args, &block)
+ args = args.inject([]) do |result, item|
if item.is_a?(Hash)
item.each { |p, v| result << [p, v] }
else
@@ -36,5 +17,18 @@ module Puppet::DSL::ResourceTypeAPI
end
result
end
+ @__created_ast_objects__.push Puppet::Parser::AST::Definition.new(name, {:arguments => args}, &block)
+ nil
+ end
+
+ def hostclass(name, options = {}, &block)
+ @__created_ast_objects__.push Puppet::Parser::AST::Hostclass.new(name, options, &block)
+ nil
+ end
+
+ def node(name, options = {}, &block)
+ name = [name] unless name.is_a?(Array)
+ @__created_ast_objects__.push Puppet::Parser::AST::Node.new(name, options, &block)
+ nil
end
end
diff --git a/lib/puppet/parser/ast/definition.rb b/lib/puppet/parser/ast/definition.rb
index 09f52b519..985f8f286 100644
--- a/lib/puppet/parser/ast/definition.rb
+++ b/lib/puppet/parser/ast/definition.rb
@@ -1,12 +1,15 @@
require 'puppet/parser/ast/top_level_construct'
class Puppet::Parser::AST::Definition < Puppet::Parser::AST::TopLevelConstruct
- def initialize(name, context = {})
+ def initialize(name, context = {}, &ruby_code)
@name = name
@context = context
+ @ruby_code = ruby_code
end
def instantiate(modname)
- return [Puppet::Resource::Type.new(:definition, @name, @context.merge(:module_name => modname))]
+ new_definition = Puppet::Resource::Type.new(:definition, @name, @context.merge(:module_name => modname))
+ new_definition.ruby_code = @ruby_code if @ruby_code
+ [new_definition]
end
end
diff --git a/lib/puppet/parser/ast/hostclass.rb b/lib/puppet/parser/ast/hostclass.rb
index d539e4deb..cab5e4a24 100644
--- a/lib/puppet/parser/ast/hostclass.rb
+++ b/lib/puppet/parser/ast/hostclass.rb
@@ -3,13 +3,16 @@ require 'puppet/parser/ast/top_level_construct'
class Puppet::Parser::AST::Hostclass < Puppet::Parser::AST::TopLevelConstruct
attr_accessor :name, :context
- def initialize(name, context = {})
+ def initialize(name, context = {}, &ruby_code)
@context = context
@name = name
+ @ruby_code = ruby_code
end
def instantiate(modname)
- all_types = [Puppet::Resource::Type.new(:hostclass, @name, @context.merge(:module_name => modname))]
+ new_class = Puppet::Resource::Type.new(:hostclass, @name, @context.merge(:module_name => modname))
+ new_class.ruby_code = @ruby_code if @ruby_code
+ all_types = [new_class]
if code
code.each do |nested_ast_node|
if nested_ast_node.respond_to? :instantiate
diff --git a/lib/puppet/parser/ast/node.rb b/lib/puppet/parser/ast/node.rb
index c19a24ce0..9767399f7 100644
--- a/lib/puppet/parser/ast/node.rb
+++ b/lib/puppet/parser/ast/node.rb
@@ -3,15 +3,18 @@ require 'puppet/parser/ast/top_level_construct'
class Puppet::Parser::AST::Node < Puppet::Parser::AST::TopLevelConstruct
attr_accessor :names
- def initialize(names, context = {})
+ def initialize(names, context = {}, &ruby_code)
raise ArgumentError, "names should be an array" unless names.is_a? Array
@names = names
@context = context
+ @ruby_code = ruby_code
end
def instantiate(modname)
@names.collect do |name|
- Puppet::Resource::Type.new(:node, name, @context.merge(:module_name => modname))
+ new_node = Puppet::Resource::Type.new(:node, name, @context.merge(:module_name => modname))
+ new_node.ruby_code = @ruby_code if @ruby_code
+ new_node
end
end
end
diff --git a/lib/puppet/parser/parser_support.rb b/lib/puppet/parser/parser_support.rb
index 859897a16..a9df33f8b 100644
--- a/lib/puppet/parser/parser_support.rb
+++ b/lib/puppet/parser/parser_support.rb
@@ -155,8 +155,7 @@ class Puppet::Parser::Parser
# how should I do error handling here?
def parse(string = nil)
if self.file =~ /\.rb$/
- parse_ruby_file
- main = nil
+ main = parse_ruby_file
else
self.string = string if string
begin
@@ -196,7 +195,13 @@ class Puppet::Parser::Parser
end
def parse_ruby_file
- require self.file
+ # Execute the contents of the file inside its own "main" object so
+ # that it can call methods in the resource type API.
+ main_object = Puppet::DSL::ResourceTypeAPI.new
+ main_object.instance_eval(File.read(self.file))
+
+ # Then extract any types that were created.
+ Puppet::Parser::AST::ASTArray.new :children => main_object.instance_eval { @__created_ast_objects__ }
end
def string=(string)
diff --git a/spec/integration/parser/ruby_manifest_spec.rb b/spec/integration/parser/ruby_manifest_spec.rb
new file mode 100644
index 000000000..6627a2456
--- /dev/null
+++ b/spec/integration/parser/ruby_manifest_spec.rb
@@ -0,0 +1,126 @@
+#!/usr/bin/env ruby
+
+require File.dirname(__FILE__) + '/../../spec_helper'
+
+require 'puppet_spec/files'
+
+describe "Pure ruby manifests" do
+ include PuppetSpec::Files
+
+ before do
+ @node = Puppet::Node.new "testnode"
+
+ @scope_resource = stub 'scope_resource', :builtin? => true, :finish => nil, :ref => 'Class[main]'
+ @scope = stub 'scope', :resource => @scope_resource, :source => mock("source")
+ @test_dir = tmpdir('ruby_manifest_test')
+ end
+
+ after do
+ Puppet.settings.clear
+ end
+
+ def write_file(name, contents)
+ path = File.join(@test_dir, name)
+ File.open(path, "w") { |f| f.write(contents) }
+ path
+ end
+
+ def compile(contents)
+ Puppet[:code] = contents
+ Dir.chdir(@test_dir) do
+ Puppet::Parser::Compiler.compile(Puppet::Node.new("mynode"))
+ end
+ end
+
+ it "should allow classes" do
+ write_file('foo.rb', ["hostclass 'one' do notify('one_notify') end",
+ "hostclass 'two' do notify('two_notify') end"].join("\n"))
+ catalog = compile("import 'foo'\ninclude one")
+ catalog.resource("Notify[one_notify]").should_not be_nil
+ catalog.resource("Notify[two_notify]").should be_nil
+ end
+
+ it "should allow defines" do
+ write_file('foo.rb', 'define "foo", :arg do notify("foo_#{@name}_#{@arg}") end')
+ catalog = compile("import 'foo'\nfoo { instance: arg => 'xyz' }")
+ catalog.resource("Notify[foo_instance_xyz]").should_not be_nil
+ catalog.resource("Foo[instance]").should_not be_nil
+ end
+
+ it "should allow node declarations" do
+ write_file('foo.rb', ["node 'mynode' do notify('mynode') end",
+ "node 'theirnode' do notify('theirnode') end"].join("\n"))
+ catalog = compile("import 'foo'")
+ catalog.resource("Notify[mynode]").should_not be_nil
+ catalog.resource("Notify[theirnode]").should be_nil
+ end
+
+ it "should allow access to the environment" do
+ write_file('foo.rb', ["hostclass 'foo' do",
+ " if environment.is_a? Puppet::Node::Environment",
+ " notify('success')",
+ " end",
+ "end"].join("\n"))
+ compile("import 'foo'\ninclude foo").resource("Notify[success]").should_not be_nil
+ end
+
+ it "should allow creation of built-in types" do
+ write_file('foo.rb', "hostclass 'foo' do file 'test_file', :owner => 'root', :mode => '644' end")
+ catalog = compile("import 'foo'\ninclude foo")
+ file = catalog.resource("File[test_file]")
+ file.should be_a Puppet::Resource
+ file.type.should == 'File'
+ file.title.should == 'test_file'
+ file.exported.should_not be
+ file.virtual.should_not be
+ file[:owner].should == 'root'
+ file[:mode].should == '644'
+ file[:stage].should be_nil # TODO: is this correct behavior?
+ end
+
+ it "should allow calling user-defined functions" do
+ write_file('foo.rb', "hostclass 'foo' do user_func :arg => 'xyz' end")
+ catalog = compile(['define user_func($arg) { notify {"n_$arg": } }',
+ 'user_func { "one": arg => "two" }'].join("\n"))
+ catalog.resource("Notify[n_two]").should_not be_nil
+ catalog.resource("User_func[one]").should_not be_nil
+ end
+
+ it "should be properly cached for multiple compiles" do
+ # Note: we can't test this by calling compile() twice, because
+ # that sets Puppet[:code], which clears out all cached
+ # environments.
+ Puppet[:filetimeout] = 1000
+ write_file('foo.rb', "hostclass 'foo' do notify('success') end")
+ Puppet[:code] = "import 'foo'\ninclude foo"
+
+ # Compile the catalog and check it
+ catalog = Dir.chdir(@test_dir) do
+ Puppet::Parser::Compiler.compile(Puppet::Node.new("mynode"))
+ end
+ catalog.resource("Notify[success]").should_not be_nil
+
+ # Secretly change the file to make it invalid. This change
+ # shouldn't be noticed because the we've set a high
+ # Puppet[:filetimeout].
+ write_file('foo.rb', "raise 'should not be executed'")
+
+ # Compile the catalog a second time and make sure it's still ok.
+ catalog = Dir.chdir(@test_dir) do
+ Puppet::Parser::Compiler.compile(Puppet::Node.new("mynode"))
+ end
+ catalog.resource("Notify[success]").should_not be_nil
+ end
+
+ it "should be properly reloaded when stale" do
+ Puppet[:filetimeout] = -1 # force stale check to happen all the time
+ write_file('foo.rb', "hostclass 'foo' do notify('version1') end")
+ catalog = compile("import 'foo'\ninclude foo")
+ catalog.resource("Notify[version1]").should_not be_nil
+ sleep 1 # so that timestamp will change forcing file reload
+ write_file('foo.rb', "hostclass 'foo' do notify('version2') end")
+ catalog = compile("import 'foo'\ninclude foo")
+ catalog.resource("Notify[version1]").should be_nil
+ catalog.resource("Notify[version2]").should_not be_nil
+ end
+end
diff --git a/spec/unit/dsl/resource_type_api_spec.rb b/spec/unit/dsl/resource_type_api_spec.rb
index 4b8ccf5a7..c9a5d272f 100755
--- a/spec/unit/dsl/resource_type_api_spec.rb
+++ b/spec/unit/dsl/resource_type_api_spec.rb
@@ -4,41 +4,51 @@ require File.dirname(__FILE__) + '/../../spec_helper'
require 'puppet/dsl/resource_type_api'
-class DSLAPITester
- include Puppet::DSL::ResourceTypeAPI
-end
-
describe Puppet::DSL::ResourceTypeAPI do
- before do
- @api = DSLAPITester.new
+ # Verify that the block creates a single AST node through the API,
+ # instantiate that AST node into a types, and return that type.
+ def test_api_call(&block)
+ main_object = Puppet::DSL::ResourceTypeAPI.new
+ main_object.instance_eval(&block)
+ created_ast_objects = main_object.instance_eval { @__created_ast_objects__ }
+ created_ast_objects.length.should == 1
+ new_types = created_ast_objects[0].instantiate('')
+ new_types.length.should == 1
+ new_types[0]
+ ensure
+ Thread.current[:ruby_file_parse_result] = nil
end
[:definition, :node, :hostclass].each do |type|
method = type == :definition ? "define" : type
it "should be able to create a #{type}" do
- newtype = @api.send(method, "myname")
+ newtype = test_api_call { send(method, "myname").should == nil }
newtype.should be_a(Puppet::Resource::Type)
newtype.type.should == type
end
it "should use the provided name when creating a #{type}" do
- newtype = @api.send(method, "myname")
+ newtype = test_api_call { send(method, "myname") }
newtype.name.should == "myname"
end
unless type == :definition
it "should pass in any provided options when creating a #{type}" do
- newtype = @api.send(method, "myname", :line => 200)
+ newtype = test_api_call { send(method, "myname", :line => 200) }
newtype.line.should == 200
end
end
- it "should set any provided block as the type's ruby code"
-
- it "should add the type to the current environment's known resource types"
+ it "should set any provided block as the type's ruby code" do
+ newtype = test_api_call { send(method, "myname") { 'method_result' } }
+ newtype.ruby_code.call.should == 'method_result'
+ end
end
describe "when creating a definition" do
- it "should use the provided options to define valid arguments for the resource type"
+ it "should use the provided options to define valid arguments for the resource type" do
+ newtype = test_api_call { define("myname", :arg1, :arg2) }
+ newtype.arguments.should == { 'arg1' => nil, 'arg2' => nil }
+ end
end
end
diff --git a/spec/unit/parser/parser_spec.rb b/spec/unit/parser/parser_spec.rb
index 0a61e73de..a964315ed 100755
--- a/spec/unit/parser/parser_spec.rb
+++ b/spec/unit/parser/parser_spec.rb
@@ -52,15 +52,6 @@ describe Puppet::Parser do
@parser.file = "/my/file.rb"
@parser.parse
end
-
- describe "in ruby" do
- it "should use the ruby interpreter to load the file" do
- @parser.file = "/my/file.rb"
- @parser.expects(:require).with "/my/file.rb"
-
- @parser.parse_ruby_file
- end
- end
end
describe "when parsing append operator" do