Saturday, August 18, 2012

Openshift Broker: Writing a Plugin Pt3 - Preparing to Test

FINALLY! Some Code!

The first two installments were used to create the boilerplate that goes around an Openshift Origin broker plugin. In this one we'll create the the skeleton of the plugin itself.  We'll also start writing the tests we need to verify that it's working.

MORE Boilerplate? REALLY?

There's a litte more infrastructure work to be done before we can actually start putting in implementation code.

Rubygems (or maybe just Openshift Origin rubygems?) like to have the source files arranged in a particular way.  I'm not quite sure why but I think it has to do with how Ruby require statements work.

The source files for the gem live in the lib subdirectory.  The file tree looks like this:

├── uplift-dnsmasq-plugin
│   └── uplift
│       └── dnsmasq-plugin.rb
└── uplift-dnsmasq-plugin.rb

2 directories, 2 files

The top level file just imports any other required modules and sets the superclass provider variable so that calls to the superclass instance() method will return the right sub class instance.  This is easy to reproduce:

1:  # Openshift Origin DNS plugin: DNSMasq  
2:  #  
3:  # The superclass StickShift::DnsService is defined in stickshift-controller  
4:  require "stickshift-controller"  
6:  # import the actual class definition and interface implementation  
7:  require "uplift-dnsmasq-plugin/uplift/dnsmasq_plugin.rb"  
9:  # initialize the superclass factory provider with the implementation class
10: # Shouldn't this really be a configuration value?  
11: #StickShift::DnsService.provider=Uplift::DnsMasqPlugin 

The require statement on line 7 is the one that will actually import the implementation code. Its value means that the implementation has to live at:


But what do we put there?

DNS Plugin Interface

Each plugin implements an interface to the back end service.  The DNS plugin has the simplest interface (and it's likely to get even simpler soon) so it's a good place to start.

The Openshift Origin plugin interfaces are defined in a de-facto way by the rubygem-stickshift-controller package code. Specifically, there are four ruby files down there which declare the plugin base classes and define stub methods for the interfaces.

  • application_container_proxy.rb
  • auth_service.rb
  • data_store.rb
  • dns_service.rb
(Currently the data_store "plugin" is fixed and uses the mongo_data_store.rb definition.)

As noted, the DNS service plugin interface is the simplest (which is why I'm starting there). It consists two standard class methods, an instance variable and a set of instance methods.

The class methods provide the override mechanism and factory class so that most code can just refer to a superclass instance.  The superclass is initialized with the desired subclass (using the provider=() method) during configuration and it provides subclass instances through the instance() method later.  It sounds confusing but all you really have to do is duplicate that bit of simple code.

The DnsService class definition looks like this:

1:  module StickShift  
2:   class DnsService  
3:    @ss_dns_provider = StickShift::DnsService  
5:    def self.provider=(provider_class)  
6:     @ss_dns_provider = provider_class  
7:    end  
9:    def self.instance  
11:    end  
13:    def initialize  
14:    end  
16:    def namespace_available?(namespace)  
17:     return true  
18:    end  
20:    def register_namespace(namespace)  
21:    end  
23:    def deregister_namespace(namespace)  
24:    end  
26:    def register_application(app_name, namespace, public_hostname)  
27:    end  
29:    def deregister_application(app_name, namespace)  
30:    end  
32:    def modify_application(app_name, namespace, public_hostname)  
33:    end  
35:    def publish  
36:    end  
38:    def close  
39:    end  
40:   end  
41:  end


That's all very nice and clean but it appears that the interface specs need some documentation so implementors can figure out what to do.  One thing we can do is look at a an existing implementation for clues.  What we find is this:

Two Class Methods: 

  • DnsService.provider=()  - set the subclass that will implement the interface
  • DnsService.instance() - get an instance of the implementing class (factory method) 

These two methods are used to set and then retrieve instances of the plugin class.  In current implementations  provider=() is called at the bottom of the plugin definition file.  This won't work if for some reason you want access to more than one plugin and the ability to switch from one to the other (as in for testing).  For now you can only install one plugin package.

Instance Methods: (The Meat)

The DNS Plugin is used to publish applications via the Domain Name Service.  It has a legacy use as a registry for user namespaces.  That registry function has moved to the MongoDB DataStore but the calls to the DnsService namespace* methods remain so we have to implement them.

These work on namespaces
  • namespace_available?(namespace)
  • register_namespace(namespace)
  • deregister_namespace(namespace)
These work on application DNS records
  • register_application(app_name, namespace, public_hostname)
  • deregister_application(app_name, namespace)
  • modify_application(app_name, namespace, public_hostname)
There are also two methods which are used to control the service update connection
  • publish() - called to force/allow cached updates to be sent to the DNS service
  • close() - called at the end of an update session to indicate that no more updates will be coming
These methods must at least be stubbed and may have side effects depending on the back end update protocol.  Callers need to be aware that updates may not wait until publish is called in some cases.

We're not ready to start implementing anything yet so we'll just copy the empty stubs from the interface definition file to start.

2:  #  
3:  # Implement the StickShift::DnsService interface using DNSMasq  
4:  #  
5:  require 'rubygems'  
7:  module Uplift  
8:   class DnsMasqPlugin < StickShift::DnsService  
9:    @ss_dns_provider = Uplift::DnsMasqPlugin  
12:    #[:domain_suffix]  
13:    # Rails.application.config.dns[...]  
15:    #  
16:    # Define stubs for the interface  
17:    #  
19:    def initialize(access_info = nil)  
20:    end  
22:    # Determine if a namespace is available.  
23:    # Return false if it has been reserved  
24:    # and true otherwise  
25:    def namespace_available?(namespace)  
26:     return false  
27:    end  
29:    # reserve the indicated namespace  
30:    def register_namespace(namespace)  
31:    end  
33:    # unreserve the indicated namespace  
34:    def deregister_namespace(namespace)  
35:    end  
37:    # publish the IP Name/Address of an application  
38:    def register_application(app_name, namespace, public_hostname)  
39:    end  
41:    # unpublish the IP Name/Address of an application  
42:    def deregister_application(app_name, namespace)  
43:    end  
45:    # update the IP Name/Address of an application  
46:    def modify_application(app_name, namespace, public_hostname)  
47:    end  
49:    # finalize accumulated updates (if needed)  
50:    def publish  
51:    end  
51:    end  
53:    # end communications with the server through this instance  
54:    def close  
55:    end  
57:   end  
59:  end

And Testing....

Even though this doesn't do anything yet we can still test it.  It's time.

We have an empty test file in spec/unit/uplift_dnsmasq_spec.rb.  We need to put something in it and run it and verify that the emtpy files load without errors.

There are a number of different unit testing frameworks for use with Ruby.  It comes with a test-unit, an xUnit style testing framework.  RSpec is a more recent DSL (Domain Specific Language) testing framework.   It is commonly used to test Rails applications.  You can use either one or some other, but you really should use something for unit testing.

In the previous blog entry we create a Rake task to run all RSpec tests in the spec subdirectory.  We placed a single file there so that we could observe the operation of the rake spec task.  The file is empty though. "running" the tests just indicates that there are no tests to run.  Now we can being to populate the test file.

The purpose of writing tests (at least initially) is to get them to fail.  Luckily, that's the easy part.  All we have to do is add a couple of lines to the test file:

1:  # Test each of the basic functions of the DnsMasqDnsService plugin class   
3:   # The plugin extends classes in the StickShift controller module.   
4:   require 'rubygems'   
6:   # Now load the plugin code itself (not the wrapper!)   
7:   #require 'uplift-dnsmasq-plugin/uplift/dnsmasq-plugin'   
9:   describe "DNSMasq DNS update plugin" do   
11:   it "has a forced pass" do   
12:    a = 1   
13:    a.should === 1   
14:   end   
16:   it "has a forced fail" do   
17:    a = 1   
18:    a.should === 0   
19:   end   
21: end  

The require statement for the plugin source code is still commented because we're going to hit some issues when we first try to load it.  This though will run and show 1 passing and one failing test.

 rake spec  
 /usr/bin/ruby -I ../../stickshift/common/lib -I ../../stickshift/controller/lib -I lib -S rspec ./spec/unit/uplift_dnsmasq_spec.rb  
  1) DNSMasq DNS update plugin has a forced fail  
    Failure/Error: a.should === 0  
     expected: 0  
       got: 1 (using ===)  
    # ./spec/unit/uplift_dnsmasq_spec.rb:18:in `block (2 levels) in <top (required)>'  
 Finished in 0.00285 seconds  
 2 examples, 1 failure  
 Failed examples:  
 rspec ./spec/unit/uplift_dnsmasq_spec.rb:16 # DNSMasq DNS update plugin has a forced fail  
 rake aborted!  
 /usr/bin/ruby -I ../../stickshift/common/lib -I ../../stickshift/controller/lib -I lib -S rspec ./spec/unit/uplift_dnsmasq_spec.rb failed  
 Tasks: TOP => spec  
 (See full trace by running task with --trace)  

Now we know that the test scripts are running and that both pass and fail will work - when we don't include our code.  Time to do that. If you uncomment line 7 of spec/unit/uplift_dnsmasq_spec.rb then the next run will try to load the plugin source file. And it will fail to load due to missing requirements.

 rake spec  
 /usr/bin/ruby -I ../../stickshift/common/lib -I ../../stickshift/controller/lib -I lib -S rspec ./spec/unit/uplift_dnsmasq_spec.rb  
 /home/mark/work/crankcase/uplift/dnsmasq/lib/uplift-dnsmasq-plugin/uplift/dnsmasq-plugin.rb:7:in `<module:Uplift>': uninitialized constant Uplift::StickShift (NameError)  
      from /home/mark/work/crankcase/uplift/dnsmasq/lib/uplift-dnsmasq-plugin/uplift/dnsmasq-plugin.rb:6:in `<top (required)>'  
      from /usr/share/rubygems/rubygems/custom_require.rb:36:in `require'  
      from /usr/share/rubygems/rubygems/custom_require.rb:36:in `require'  
      from /home/mark/work/crankcase/uplift/dnsmasq/spec/unit/uplift_dnsmasq_spec.rb:7:in `<top (required)>'  
      from /home/mark/.gem/ruby/1.9.1/gems/rspec-core-2.11.1/lib/rspec/core/configuration.rb:780:in `load'  
      from /home/mark/.gem/ruby/1.9.1/gems/rspec-core-2.11.1/lib/rspec/core/configuration.rb:780:in `block in load_spec_files'  
      from /home/mark/.gem/ruby/1.9.1/gems/rspec-core-2.11.1/lib/rspec/core/configuration.rb:780:in `map'  
      from /home/mark/.gem/ruby/1.9.1/gems/rspec-core-2.11.1/lib/rspec/core/configuration.rb:780:in `load_spec_files'  
      from /home/mark/.gem/ruby/1.9.1/gems/rspec-core-2.11.1/lib/rspec/core/command_line.rb:22:in `run'  
      from /home/mark/.gem/ruby/1.9.1/gems/rspec-core-2.11.1/lib/rspec/core/runner.rb:69:in `run'  
      from /home/mark/.gem/ruby/1.9.1/gems/rspec-core-2.11.1/lib/rspec/core/runner.rb:8:in `block in autorun'  
 rake aborted!  
 /usr/bin/ruby -I ../../stickshift/common/lib -I ../../stickshift/controller/lib -I lib -S rspec ./spec/unit/uplift_dnsmasq_spec.rb failed  
 Tasks: TOP => spec  
 (See full trace by running task with --trace)  

See? And that's what we'll fix next.

(No references this time, since there's really nothing new)

No comments:

Post a Comment