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:
lib
├── 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:
lib/uplift-dnsmasq-plugin.rb
1: # Openshift Origin DNS plugin: DNSMasq
2: #
3: # The superclass StickShift::DnsService is defined in stickshift-controller
4: require "stickshift-controller"
5:
6: # import the actual class definition and interface implementation
7: require "uplift-dnsmasq-plugin/uplift/dnsmasq_plugin.rb"
8:
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:
lib/uplift-dnsmasq-plugin/uplift/dnsmasq_plugin.rb
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.
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:
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
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
4:
5: def self.provider=(provider_class)
6: @ss_dns_provider = provider_class
7: end
8:
9: def self.instance
10: @ss_dns_provider.new
11: end
12:
13: def initialize
14: end
15:
16: def namespace_available?(namespace)
17: return true
18: end
19:
20: def register_namespace(namespace)
21: end
22:
23: def deregister_namespace(namespace)
24: end
25:
26: def register_application(app_name, namespace, public_hostname)
27: end
28:
29: def deregister_application(app_name, namespace)
30: end
31:
32: def modify_application(app_name, namespace, public_hostname)
33: end
34:
35: def publish
36: end
37:
38: def close
39: end
40: end
41: end
Implementation
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.
lib/uplift-dnsmasc-plugin/uplift/dnsmasq_plugin.rb
1:
2: #
3: # Implement the StickShift::DnsService interface using DNSMasq
4: #
5: require 'rubygems'
6:
7: module Uplift
8: class DnsMasqPlugin < StickShift::DnsService
9: @ss_dns_provider = Uplift::DnsMasqPlugin
10:
11: # DEPENDENCIES
12: # Rails.application.config.ss[:domain_suffix]
13: # Rails.application.config.dns[...]
14:
15: #
16: # Define stubs for the interface
17: #
18:
19: def initialize(access_info = nil)
20: end
21:
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
28:
29: # reserve the indicated namespace
30: def register_namespace(namespace)
31: end
32:
33: # unreserve the indicated namespace
34: def deregister_namespace(namespace)
35: end
36:
37: # publish the IP Name/Address of an application
38: def register_application(app_name, namespace, public_hostname)
39: end
40:
41: # unpublish the IP Name/Address of an application
42: def deregister_application(app_name, namespace)
43: end
44:
45: # update the IP Name/Address of an application
46: def modify_application(app_name, namespace, public_hostname)
47: end
48:
49: # finalize accumulated updates (if needed)
50: def publish
51: end
51: end
52:
53: # end communications with the server through this instance
54: def close
55: end
56:
57: end
58:
59: end
And Testing....
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:
spec/unit/uplift_dynect_spec.rb
1: # Test each of the basic functions of the DnsMasqDnsService plugin class
2:
3: # The plugin extends classes in the StickShift controller module.
4: require 'rubygems'
5:
6: # Now load the plugin code itself (not the wrapper!)
7: #require 'uplift-dnsmasq-plugin/uplift/dnsmasq-plugin'
8:
9: describe "DNSMasq DNS update plugin" do
10:
11: it "has a forced pass" do
12: a = 1
13: a.should === 1
14: end
15:
16: it "has a forced fail" do
17: a = 1
18: a.should === 0
19: end
20:
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
.F
Failures:
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)