diff options
-rw-r--r-- | lib/puppet/dsl.rb | 4 | ||||
-rw-r--r-- | lib/puppet/dsl/resource_type_api.rb | 26 | ||||
-rw-r--r-- | lib/puppet/parser/parser_support.rb | 4 | ||||
-rw-r--r-- | spec/integration/parser/ruby_manifest_spec.rb | 128 | ||||
-rwxr-xr-x | spec/unit/dsl/resource_type_api_spec.rb | 44 | ||||
-rwxr-xr-x | spec/unit/parser/parser_spec.rb | 9 |
6 files changed, 179 insertions, 36 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..ecb914189 100644 --- a/lib/puppet/dsl/resource_type_api.rb +++ b/lib/puppet/dsl/resource_type_api.rb @@ -1,33 +1,39 @@ require 'puppet/resource/type' -module Puppet::DSL::ResourceTypeAPI +class 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 + result = __mk_resource_type__(:definition, name, Hash.new, block) + result.set_arguments(__munge_type_arguments__(args)) + nil end def hostclass(name, options = {}, &block) - mk_resource_type(:hostclass, name, options, block) + __mk_resource_type__(:hostclass, name, options, block) + nil end def node(name, options = {}, &block) - mk_resource_type(:node, name, options, block) + __mk_resource_type__(:node, name, options, block) + nil end - private + # Note: we don't want the user to call the following methods + # directly. However, we can't stop them by making the methods + # private because the user's .rb code gets instance_eval'ed on an + # instance of this class. So instead we name the methods using + # double underscores to discourage customers from calling them. - def mk_resource_type(type, name, options, code) + 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 + Thread.current[:known_resource_types].add klass klass end - def munge_type_arguments(args) + def __munge_type_arguments__(args) args.inject([]) do |result, item| if item.is_a?(Hash) item.each { |p, v| result << [p, v] } diff --git a/lib/puppet/parser/parser_support.rb b/lib/puppet/parser/parser_support.rb index 4f3a4ddff..c90c1978f 100644 --- a/lib/puppet/parser/parser_support.rb +++ b/lib/puppet/parser/parser_support.rb @@ -215,7 +215,9 @@ 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. + Puppet::DSL::ResourceTypeAPI.new.instance_eval(File.read(self.file)) 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..af110d3b3 --- /dev/null +++ b/spec/integration/parser/ruby_manifest_spec.rb @@ -0,0 +1,128 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../spec_helper' + +require 'tempfile' +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 "bar", :arg do notify("bar_#{@name}_#{@arg}") end') + catalog = compile("import 'foo'\nbar { instance: arg => 'xyz' }") + catalog.resource("Notify[bar_instance_xyz]").should_not be_nil + catalog.resource("Bar[instance]").should_not be_nil + end + + it "should allow node declarations" do + write_file('foo.rb', "node 'mynode' do notify('mynode') end") + catalog = compile("import 'foo'") + node_declaration = catalog.resource("Notify[mynode]") + node_declaration.should_not be_nil + node_declaration.title.should == 'mynode' + end + + it "should allow access to the environment" do + write_file('foo.rb', ["hostclass 'bar' do", + " if environment.is_a? Puppet::Node::Environment", + " notify('success')", + " end", + "end"].join("\n")) + compile("import 'foo'\ninclude bar").resource("Notify[success]").should_not be_nil + end + + it "should allow creation of resources of built-in types" do + write_file('foo.rb', "hostclass 'bar' do file 'test_file', :owner => 'root', :mode => '644' end") + catalog = compile("import 'foo'\ninclude bar") + 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 'bar' do user_func 'name', :arg => 'xyz' end") + catalog = compile(['define user_func($arg) { notify {"n_$arg": } }', + 'import "foo"', + 'include bar'].join("\n")) + catalog.resource("Notify[n_xyz]").should_not be_nil + catalog.resource("User_func[name]").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 'bar' do notify('success') end") + Puppet[:code] = "import 'foo'\ninclude bar" + + # 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 'bar' do notify('version1') end") + catalog = compile("import 'foo'\ninclude bar") + catalog.resource("Notify[version1]").should_not be_nil + sleep 1 # so that timestamp will change forcing file reload + write_file('foo.rb', "hostclass 'bar' do notify('version2') end") + catalog = compile("import 'foo'\ninclude bar") + 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 5abe79ea7..4f4eb7e01 100755 --- a/spec/unit/dsl/resource_type_api_spec.rb +++ b/spec/unit/dsl/resource_type_api_spec.rb @@ -4,13 +4,14 @@ 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 + # Run the given block in the context of a new ResourceTypeAPI + # object. + def test_api_call(&block) + Thread.current[:known_resource_types] = Puppet::Resource::TypeCollection.new(:env) + Puppet::DSL::ResourceTypeAPI.new.instance_eval(&block) + ensure + Thread.current[:known_resource_types] = nil end [:definition, :node, :hostclass].each do |type| @@ -18,29 +19,48 @@ describe Puppet::DSL::ResourceTypeAPI do it "should be able to create a #{type}" do newtype = Puppet::Resource::Type.new(:hostclass, "foo") Puppet::Resource::Type.expects(:new).with { |t, n, args| t == type }.returns newtype - @api.send(method, "myname") + test_api_call { send(method, "myname") } end it "should use the provided name when creating a #{type}" do type = Puppet::Resource::Type.new(:hostclass, "foo") Puppet::Resource::Type.expects(:new).with { |t, n, args| n == "myname" }.returns type - @api.send(method, "myname") + test_api_call { send(method, "myname") } end unless type == :definition it "should pass in any provided options" do type = Puppet::Resource::Type.new(:hostclass, "foo") Puppet::Resource::Type.expects(:new).with { |t, n, args| args == {:myarg => :myvalue} }.returns type - @api.send(method, "myname", :myarg => :myvalue) + test_api_call { send(method, "myname", :myarg => :myvalue) } end end - it "should set any provided block as the type's ruby code" + it "should set any provided block as the type's ruby code" do + Puppet::Resource::Type.any_instance.expects(:ruby_code=).with { |blk| blk.call == 'foo' } + test_api_call { send(method, "myname") { 'foo' } } + end - it "should add the type to the current environment's known resource types" + it "should add the type to the current environment's known resource types" do + begin + newtype = Puppet::Resource::Type.new(:hostclass, "foo") + Puppet::Resource::Type.expects(:new).returns newtype + known_resource_types = Puppet::Resource::TypeCollection.new(:env) + Thread.current[:known_resource_types] = known_resource_types + known_resource_types.expects(:add).with(newtype) + Puppet::DSL::ResourceTypeAPI.new.instance_eval { hostclass "myname" } + ensure + Thread.current[:known_resource_types] = nil + end + 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 = Puppet::Resource::Type.new(:definition, "foo") + Puppet::Resource::Type.expects(:new).returns newtype + test_api_call { define("myname", :arg1, :arg2) } + newtype.instance_eval { @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 0657ab37a..f73e07a5c 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 |