diff options
author | Luke Kanies <luke@reductivelabs.com> | 2010-01-08 00:49:36 -0800 |
---|---|---|
committer | test branch <puppet-dev@googlegroups.com> | 2010-02-17 06:50:53 -0800 |
commit | 6d2a10b40c9f77ea5101abe6e568ed5a798c04f3 (patch) | |
tree | b8ef1e9860be5c7b558e210fa066d327e5dbab92 | |
parent | e515513b5cb065f3e43f5c2880d0452d8e2b25b8 (diff) | |
download | puppet-6d2a10b40c9f77ea5101abe6e568ed5a798c04f3.tar.gz puppet-6d2a10b40c9f77ea5101abe6e568ed5a798c04f3.tar.xz puppet-6d2a10b40c9f77ea5101abe6e568ed5a798c04f3.zip |
Adding simplistic pure ruby interface
This is a simplistic DSL - you can create
resource types (defined resources), classes,
and nodes, and they can call functions and
create resources. Nothing else, at this point.
Signed-off-by: Luke Kanies <luke@reductivelabs.com>
-rw-r--r-- | ext/pure_ruby_dsl/dsl_test.rb | 7 | ||||
-rw-r--r-- | lib/puppet/dsl.rb | 11 | ||||
-rw-r--r-- | lib/puppet/dsl/resource_helper.rb | 62 | ||||
-rw-r--r-- | lib/puppet/dsl/resource_type_api.rb | 40 | ||||
-rw-r--r-- | lib/puppet/resource/type.rb | 18 | ||||
-rw-r--r-- | lib/puppet/resource/type_collection.rb | 2 | ||||
-rwxr-xr-x | spec/unit/dsl/resource_helper.rb | 151 | ||||
-rwxr-xr-x | spec/unit/dsl/resource_type_api.rb | 46 | ||||
-rwxr-xr-x | spec/unit/resource/type.rb | 39 | ||||
-rw-r--r-- | spec/unit/resource/type_collection.rb | 2 |
10 files changed, 367 insertions, 11 deletions
diff --git a/ext/pure_ruby_dsl/dsl_test.rb b/ext/pure_ruby_dsl/dsl_test.rb new file mode 100644 index 000000000..6eff2982e --- /dev/null +++ b/ext/pure_ruby_dsl/dsl_test.rb @@ -0,0 +1,7 @@ +hostclass "foobar" do + notify "this is a test", "loglevel" => "warning" +end + +node "default" do + acquire "foobar" +end diff --git a/lib/puppet/dsl.rb b/lib/puppet/dsl.rb new file mode 100644 index 000000000..93cf0fbcf --- /dev/null +++ b/lib/puppet/dsl.rb @@ -0,0 +1,11 @@ +require 'puppet' + +module Puppet::DSL +end + +require 'puppet/dsl/resource_type_api' +require 'puppet/dsl/resource_helper' + +class Object + include Puppet::DSL::ResourceTypeAPI +end diff --git a/lib/puppet/dsl/resource_helper.rb b/lib/puppet/dsl/resource_helper.rb new file mode 100644 index 000000000..aaf8ab5f7 --- /dev/null +++ b/lib/puppet/dsl/resource_helper.rb @@ -0,0 +1,62 @@ +# This module adds functionality to a resource to make it +# capable of evaluating the DSL resource type block and also +# hooking into the scope system. +require 'puppet/resource/type_collection_helper' + +module Puppet::DSL::ResourceHelper + include Puppet::Resource::TypeCollectionHelper + + FUNCTION_MAP = {:acquire => :include} + + # Try to convert a missing method into a resource type or a function. + def method_missing(name, *args) + return create_resource(name, args[0], args[1]) if valid_type?(name) + + name = map_function(name) + + return call_function(name, args) if Puppet::Parser::Functions.function(name) + + super + end + + def set_instance_variables + eachparam do |param| + instance_variable_set("@#{param.name}", param.value) + end + end + + def create_resource(type, names, arguments = nil) + names = [names] unless names.is_a?(Array) + + arguments ||= {} + raise ArgumentError, "Resource arguments must be provided as a hash" unless arguments.is_a?(Hash) + + names.collect do |name| + resource = Puppet::Parser::Resource.new(:type => type, :title => name, :scope => scope) + arguments.each do |param, value| + resource[param] = value + end + + scope.compiler.add_resource(scope, resource) + resource + end + end + + def call_function(name, args) + return false unless method = Puppet::Parser::Functions.function(name) + scope.send(method, *args) + end + + def valid_type?(name) + return true if [:class, :node].include?(name) + return true if Puppet::Type.type(name) + return true if known_resource_types.definition(name) + return false + end + + private + + def map_function(name) + return FUNCTION_MAP[name] || name + end +end diff --git a/lib/puppet/dsl/resource_type_api.rb b/lib/puppet/dsl/resource_type_api.rb new file mode 100644 index 000000000..014d1a3dc --- /dev/null +++ b/lib/puppet/dsl/resource_type_api.rb @@ -0,0 +1,40 @@ +require 'puppet/resource/type' + +module Puppet::DSL::ResourceTypeAPI + def resource_type(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 + end + + def munge_type_arguments(args) + args.inject([]) do |result, item| + if item.is_a?(Hash) + item.each { |p, v| result << [p, v] } + else + result << item + end + result + end + end +end diff --git a/lib/puppet/resource/type.rb b/lib/puppet/resource/type.rb index 1b78bf58b..fa5fd5c88 100644 --- a/lib/puppet/resource/type.rb +++ b/lib/puppet/resource/type.rb @@ -3,6 +3,7 @@ require 'puppet/util/warnings' require 'puppet/util/errors' require 'puppet/util/inline_docs' require 'puppet/parser/ast/leaf' +require 'puppet/dsl' class Puppet::Resource::Type include Puppet::Util::InlineDocs @@ -11,7 +12,7 @@ class Puppet::Resource::Type RESOURCE_SUPERTYPES = [:hostclass, :node, :definition] - attr_accessor :file, :line, :doc, :code, :parent, :code_collection + attr_accessor :file, :line, :doc, :code, :ruby_code, :parent, :resource_type_collection attr_reader :type, :namespace, :arguments, :behaves_like # Are we a child of the passed class? Do a recursive search up our @@ -35,8 +36,9 @@ class Puppet::Resource::Type set_resource_parameters(resource, scope) - return nil unless c = self.code - return c.safeevaluate(scope) + code.safeevaluate(scope) if code + + evaluate_ruby_code(resource, scope) if ruby_code end def initialize(type, name, options = {}) @@ -137,7 +139,7 @@ class Puppet::Resource::Type def parent_type return nil unless parent - unless @parent_type ||= code_collection.send(type, parent) + unless @parent_type ||= resource_type_collection.send(type, parent) fail Puppet::ParseError, "Could not find parent resource type '#{parent}'" end @@ -217,6 +219,14 @@ class Puppet::Resource::Type return parent_scope(resource.scope, klass) end + def evaluate_ruby_code(resource, scope) + resource.extend(Puppet::DSL::ResourceHelper) + + resource.set_instance_variables + + resource.instance_eval(&ruby_code) + end + # Split an fq name into a namespace and name def namesplit(fullname) ary = fullname.split("::") diff --git a/lib/puppet/resource/type_collection.rb b/lib/puppet/resource/type_collection.rb index e2ca56290..513c1c672 100644 --- a/lib/puppet/resource/type_collection.rb +++ b/lib/puppet/resource/type_collection.rb @@ -31,7 +31,7 @@ class Puppet::Resource::TypeCollection end method = "add_#{instance.type}" send(method, instance) - instance.code_collection = self + instance.resource_type_collection = self instance end diff --git a/spec/unit/dsl/resource_helper.rb b/spec/unit/dsl/resource_helper.rb new file mode 100755 index 000000000..11dad40cf --- /dev/null +++ b/spec/unit/dsl/resource_helper.rb @@ -0,0 +1,151 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../spec_helper' + +require 'puppet/dsl/resource_helper' + +class DSLResourceHelperTester + include Puppet::DSL::ResourceHelper +end + +describe Puppet::DSL::ResourceHelper do + before do + @resource = Puppet::Parser::Resource.new(:type => :mytype, :title => "myresource", :scope => mock("scope"), :source => mock("source")) + class << @resource + include Puppet::DSL::ResourceHelper + end + end + + it "should include the resource type collection helper" do + Puppet::DSL::ResourceHelper.ancestors.should be_include(Puppet::Resource::TypeCollectionHelper) + end + + it "should be able to set all of its parameters as instance variables" do + @resource["foo"] = "myval" + @resource.set_instance_variables + @resource.instance_variable_get("@foo").should == "myval" + end + + describe "when calling a function" do + it "should return false if the function does not exist" do + Puppet::Parser::Functions.expects(:function).with("myfunc").returns nil + @resource.call_function("myfunc", "foo").should be_false + end + + it "should use the scope the call the provided function with the provided arguments and return the results" do + scope = stub 'scope' + @resource.stubs(:scope).returns scope + Puppet::Parser::Functions.expects(:function).with("myfunc").returns "myfunc_method" + + scope.expects(:myfunc_method).with("one", "two") + @resource.call_function("myfunc", ["one", "two"]) + end + end + + describe "when determining if a provided name is a valid type" do + it "should be valid if it's :class" do + @resource.should be_valid_type(:class) + end + + it "should be valid if it's :node" do + @resource.should be_valid_type(:node) + end + + it "should be valid if it's a builtin type" do + Puppet::Type.expects(:type).with(:mytype).returns "whatever" + @resource.should be_valid_type(:mytype) + end + + it "should be valid if it's a defined resource type in the environment's known resource types" do + collection = stub 'collection' + @resource.stubs(:known_resource_types).returns collection + collection.expects(:definition).with(:mytype).returns "whatever" + @resource.should be_valid_type(:mytype) + end + + it "should not be valid unless it's a node, class, builtin type, or defined resource" do + collection = stub 'collection' + @resource.stubs(:known_resource_types).returns collection + collection.expects(:definition).returns nil + Puppet::Type.expects(:type).returns nil + @resource.should_not be_valid_type(:mytype) + end + end + + describe "when creating a resource" do + before do + @resource.scope.stubs(:source).returns stub("source") + @resource.scope.stubs(:compiler).returns stub("compiler", :add_resource => nil) + @created_resource = Puppet::Parser::Resource.new(:title => "eh", :type => 'yay', :scope => @resource.scope) + end + + it "should create and return a resource of the type specified" do + Puppet::Parser::Resource.expects(:new).with { |args| args[:type] == "mytype" }.returns @created_resource + @resource.create_resource("mytype", "myname", {:foo => "bar"}).should == [@created_resource] + end + + it "should use the name from the first element of the provided argument array" do + Puppet::Parser::Resource.expects(:new).with { |args| args[:title] == "myname" }.returns @created_resource + @resource.create_resource("mytype", "myname", {:foo => "bar"}) + end + + it "should create multiple resources if the first element of the argument array is an array" do + second_resource = Puppet::Parser::Resource.new(:title => "eh", :type => 'yay', :scope => @resource.scope) + Puppet::Parser::Resource.expects(:new).with { |args| args[:title] == "first" }.returns @created_resource + Puppet::Parser::Resource.expects(:new).with { |args| args[:title] == "second" }.returns @created_resource + @resource.create_resource("mytype", ["first", "second"], {:foo => "bar"}) + end + + it "should provide its scope as the scope" do + Puppet::Parser::Resource.expects(:new).with { |args| args[:scope] == @resource.scope }.returns @created_resource + @resource.create_resource("mytype", "myname", {:foo => "bar"}) + end + + it "should set each provided argument as a parameter on the created resource" do + result = @resource.create_resource("mytype", "myname", {"foo" => "bar", "biz" => "baz"}).shift + result["foo"].should == "bar" + result["biz"].should == "baz" + end + + it "should add the resource to the scope's copmiler" do + Puppet::Parser::Resource.expects(:new).returns @created_resource + @resource.scope.compiler.expects(:add_resource).with(@resource.scope, @created_resource) + @resource.create_resource("mytype", "myname", {:foo => "bar"}) + end + + it "should fail if the resource parameters are not a hash" do + lambda { @resource.create_resource("mytype", "myname", %w{foo bar}) }.should raise_error(ArgumentError) + end + end + + describe "when an unknown method is called" do + it "should create a resource if the method name is a valid type" do + @resource.expects(:valid_type?).with(:mytype).returns true + @resource.expects(:create_resource).with(:mytype, "myname", {:foo => "bar"}).returns true + + @resource.mytype("myname", :foo => "bar") + end + + it "should call any function whose name matches the undefined method if the name is not a valid type" do + @resource.expects(:valid_type?).with(:myfunc).returns false + @resource.expects(:create_resource).never + + Puppet::Parser::Functions.expects(:function).with(:myfunc).returns true + + @resource.expects(:call_function).with(:myfunc, %w{foo bar}) + + @resource.myfunc("foo", "bar") + end + + it "should raise a method missing error if the method is neither a type nor a function" do + @resource.expects(:valid_type?).with(:myfunc).returns false + @resource.expects(:create_resource).never + + Puppet::Parser::Functions.expects(:function).with(:myfunc).returns false + + @resource.expects(:call_function).never + + lambda { @resource.myfunc("foo", "bar") }.should raise_error(NoMethodError) + end + end +end diff --git a/spec/unit/dsl/resource_type_api.rb b/spec/unit/dsl/resource_type_api.rb new file mode 100755 index 000000000..8d63bf28e --- /dev/null +++ b/spec/unit/dsl/resource_type_api.rb @@ -0,0 +1,46 @@ +#!/usr/bin/env ruby + +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 + end + + [:definition, :node, :hostclass].each do |type| + method = type == :definition ? "resource_type" : type + 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") + 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") + 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) + 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" + end + + describe "when creating a definition" do + it "should use the provided options to define valid arguments for the resource type" + end +end diff --git a/spec/unit/resource/type.rb b/spec/unit/resource/type.rb index d1ee024ef..bb372177d 100755 --- a/spec/unit/resource/type.rb +++ b/spec/unit/resource/type.rb @@ -9,7 +9,7 @@ describe Puppet::Resource::Type do Puppet::Resource::Type.new(:hostclass, "foo").name.should == "foo" end - [:code, :doc, :line, :file, :code_collection].each do |attr| + [:code, :doc, :line, :file, :resource_type_collection, :ruby_code].each do |attr| it "should have a '#{attr}' attribute" do type = Puppet::Resource::Type.new(:hostclass, "foo") type.send(attr.to_s + "=", "yay") @@ -94,19 +94,19 @@ describe Puppet::Resource::Type do end it "should return the name converted to a string when the name is not a regex" do - pending "Need to define ResourceTypeCollection behaviour first" + pending "Need to define LoadedCode behaviour first" name = Puppet::Parser::AST::HostName.new(:value => "foo") Puppet::Resource::Type.new(:node, name).name.should == "foo" end it "should return the name converted to a string when the name is a regex" do - pending "Need to define ResourceTypeCollection behaviour first" + pending "Need to define LoadedCode behaviour first" name = Puppet::Parser::AST::HostName.new(:value => /regex/) Puppet::Resource::Type.new(:node, name).name.should == /regex/.to_s end it "should mark any created scopes as a node scope" do - pending "Need to define ResourceTypeCollection behaviour first" + pending "Need to define LoadedCode behaviour first" name = Puppet::Parser::AST::HostName.new(:value => /regex/) Puppet::Resource::Type.new(:node, name).name.should == /regex/.to_s end @@ -330,6 +330,8 @@ describe Puppet::Resource::Type do @compiler = Puppet::Parser::Compiler.new(Puppet::Node.new("mynode")) @scope = Puppet::Parser::Scope.new :compiler => @compiler @resource = Puppet::Parser::Resource.new(:foo, "yay", :scope => @scope) + @known_resource_types = stub 'known_resource_types' + @resource.stubs(:known_resource_types).returns @known_resource_types @type = Puppet::Resource::Type.new(:hostclass, "foo") @type.stubs(:set_resource_parameters) end @@ -359,7 +361,7 @@ describe Puppet::Resource::Type do @compiler.class_scope(@type).should be_nil end - it "should evaluate the code if any is provided" do + it "should evaluate the AST code if any is provided" do code = stub 'code' @type.stubs(:code).returns code @type.stubs(:subscope).returns stub("subscope", :compiler => @compiler) @@ -368,6 +370,33 @@ describe Puppet::Resource::Type do @type.evaluate_code(@resource) end + describe "and ruby code is provided" do + before do + @type.stubs(:ruby_code).returns(proc { "foo" }) + end + + it "should instance evaluate the ruby code on the resource" do + evaluated = false + @type.stubs(:ruby_code).returns(proc { evaluated = true }) + + @type.evaluate_code(@resource) + + evaluated.should be_true + end + + it "should include the DSL Resource Helper module in the provided resource" do + @type.evaluate_code(@resource) + + @resource.metaclass.ancestors.should be_include(Puppet::DSL::ResourceHelper) + end + + it "should convert the resource's parameters to instance variables" do + @resource.expects(:set_instance_variables) + + @type.evaluate_code(@resource) + end + end + it "should noop if there is no code" do @type.expects(:code).returns nil diff --git a/spec/unit/resource/type_collection.rb b/spec/unit/resource/type_collection.rb index a2a213f5a..ea78afc60 100644 --- a/spec/unit/resource/type_collection.rb +++ b/spec/unit/resource/type_collection.rb @@ -40,7 +40,7 @@ describe Puppet::Resource::TypeCollection do @code.add(node) @code.node("foo").should equal(node) - node.code_collection.should equal(@code) + node.resource_type_collection.should equal(@code) end it "should store node resource types as nodes" do |