diff options
-rw-r--r-- | lib/puppet/dsl.rb | 4 | ||||
-rw-r--r-- | lib/puppet/dsl/resource_type_api.rb | 48 | ||||
-rw-r--r-- | lib/puppet/parser/ast/definition.rb | 7 | ||||
-rw-r--r-- | lib/puppet/parser/ast/hostclass.rb | 7 | ||||
-rw-r--r-- | lib/puppet/parser/ast/node.rb | 7 | ||||
-rw-r--r-- | lib/puppet/parser/parser_support.rb | 11 | ||||
-rw-r--r-- | spec/integration/parser/ruby_manifest_spec.rb | 126 | ||||
-rwxr-xr-x | spec/unit/dsl/resource_type_api_spec.rb | 36 | ||||
-rwxr-xr-x | spec/unit/parser/parser_spec.rb | 9 |
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 |