From 5fdf659df10651ffd50cccec505b65a113d66b13 Mon Sep 17 00:00:00 2001 From: Steven Danna Date: Wed, 31 Aug 2016 12:17:44 +0100 Subject: [PATCH] Load all dependent libraries, even if include_context isn't called The goal of these changes is to ensure that the libraries from dependencies are loaded even if their controls are never included. To facilitate this, we break up the loading into seperate steps, and move the loading code into the Profile which has acceess to the dependency information. Signed-off-by: Steven Danna --- lib/inspec/base_cli.rb | 2 +- lib/inspec/dependencies/dependency_set.rb | 27 ++++-- lib/inspec/dependencies/requirement.rb | 19 ++-- lib/inspec/dependencies/resolver.rb | 4 +- lib/inspec/dsl.rb | 8 +- lib/inspec/profile.rb | 87 +++++++++++++------ lib/inspec/profile_context.rb | 51 +++++------ lib/inspec/rule.rb | 4 + lib/inspec/runner.rb | 87 ++++++++----------- test/docker_test.rb | 2 +- test/helper.rb | 1 + test/unit/dsl/control_test.rb | 2 +- .../profile_b/controls/example.rb | 2 +- .../profile_c/libraries/gordon_config.rb | 45 +--------- .../profile_d/libraries/gordon_config.rb | 47 +--------- 15 files changed, 167 insertions(+), 221 deletions(-) diff --git a/lib/inspec/base_cli.rb b/lib/inspec/base_cli.rb index e3cdf1d43..9df414fe9 100644 --- a/lib/inspec/base_cli.rb +++ b/lib/inspec/base_cli.rb @@ -70,7 +70,7 @@ module Inspec o[:logger].level = get_log_level(o.log_level) runner = Inspec::Runner.new(o) - targets.each { |target| runner.add_target(target, opts) } + targets.each { |target| runner.add_target(target) } exit runner.run rescue RuntimeError, Train::UserError => e $stderr.puts e.message diff --git a/lib/inspec/dependencies/dependency_set.rb b/lib/inspec/dependencies/dependency_set.rb index 4a4ab3700..1ad9621e1 100644 --- a/lib/inspec/dependencies/dependency_set.rb +++ b/lib/inspec/dependencies/dependency_set.rb @@ -18,14 +18,22 @@ module Inspec # @param cwd [String] Current working directory for relative path includes # @param vendor_path [String] Path to the vendor directory # - def self.from_lockfile(lockfile, cwd, vendor_path) + def self.from_lockfile(lockfile, cwd, vendor_path, backend) vendor_index = VendorIndex.new(vendor_path) dep_tree = lockfile.deps.map do |dep| - Inspec::Requirement.from_lock_entry(dep, cwd, vendor_index) + Inspec::Requirement.from_lock_entry(dep, cwd, vendor_index, backend) end dep_list = flatten_dep_tree(dep_tree) - new(cwd, vendor_path, dep_list) + new(cwd, vendor_path, dep_list, backend) + end + + def self.from_array(dependencies, cwd, vendor_path, backend) + dep_list = {} + dependencies.each do |d| + dep_list[d.name] = d + end + new(cwd, vendor_path, dep_list, backend) end # This is experimental code to test the working of the @@ -50,14 +58,21 @@ module Inspec # @param cwd [String] current working directory for relative path includes # @param vendor_path [String] path which contains vendored dependencies # @return [dependencies] this - def initialize(cwd, vendor_path, dep_list = nil) + def initialize(cwd, vendor_path, dep_list, backend) @cwd = cwd @vendor_path = vendor_path @dep_list = dep_list + @backend = backend + end + + def each + @dep_list.each do |_k, v| + yield v.profile + end end def list - @dep_list + @dep_list || {} end def to_array @@ -77,7 +92,7 @@ module Inspec def vendor(dependencies) return nil if dependencies.nil? || dependencies.empty? @vendor_index ||= VendorIndex.new(@vendor_path) - @dep_list = Resolver.resolve(dependencies, @vendor_index, @cwd) + @dep_list = Resolver.resolve(dependencies, @vendor_index, @cwd, @backend) end end end diff --git a/lib/inspec/dependencies/requirement.rb b/lib/inspec/dependencies/requirement.rb index 25b74c64d..2c27e2275 100644 --- a/lib/inspec/dependencies/requirement.rb +++ b/lib/inspec/dependencies/requirement.rb @@ -1,5 +1,6 @@ # encoding: utf-8 require 'inspec/fetcher' +require 'inspec/dependencies/dependency_set' require 'digest' module Inspec @@ -18,17 +19,18 @@ module Inspec new(name, version, vendor_index, opts[:cwd], opts.merge(dep)) end - def self.from_lock_entry(entry, cwd, vendor_index) + def self.from_lock_entry(entry, cwd, vendor_index, backend) req = new(entry['name'], entry['version_constraints'], vendor_index, - cwd, { url: entry['resolved_source'] }) + cwd, + { url: entry['resolved_source'], + backend: backend }) locked_deps = [] Array(entry['dependencies']).each do |dep_entry| - locked_deps << Inspec::Requirement.from_lock_entry(dep_entry, cwd, vendor_index) + locked_deps << Inspec::Requirement.from_lock_entry(dep_entry, cwd, vendor_index, backend) end - req.lock_deps(locked_deps) req end @@ -38,6 +40,7 @@ module Inspec @version_requirement = Gem::Requirement.new(Array(version_constraints)) @dep = Gem::Dependency.new(name, @version_requirement, :runtime) @vendor_index = vendor_index + @backend = opts[:backend] @opts = opts @cwd = cwd end @@ -123,7 +126,7 @@ module Inspec def dependencies @dependencies ||= profile.metadata.dependencies.map do |r| - Inspec::Requirement.from_metadata(r, @vendor_index, cwd: @cwd) + Inspec::Requirement.from_metadata(r, @vendor_index, cwd: @cwd, backend: @backend) end end @@ -137,7 +140,11 @@ module Inspec def profile return nil if path.nil? - @profile ||= Inspec::Profile.for_target(path, {}) + opts = { backend: @backend } + if !@dependencies.nil? + opts[:dependencies] = Inspec::DependencySet.from_array(@dependencies, @cwd, @vendor_index, @backend) + end + @profile ||= Inspec::Profile.for_target(path, opts) end end end diff --git a/lib/inspec/dependencies/resolver.rb b/lib/inspec/dependencies/resolver.rb index 5e3371b18..d43244a5c 100644 --- a/lib/inspec/dependencies/resolver.rb +++ b/lib/inspec/dependencies/resolver.rb @@ -23,9 +23,9 @@ module Inspec # implementation of the fetcher being used. # class Resolver - def self.resolve(dependencies, vendor_index, working_dir) + def self.resolve(dependencies, vendor_index, working_dir, backend) reqs = dependencies.map do |dep| - req = Inspec::Requirement.from_metadata(dep, vendor_index, cwd: working_dir) + req = Inspec::Requirement.from_metadata(dep, vendor_index, cwd: working_dir, backend: backend) req || fail("Cannot initialize dependency: #{req}") end new.resolve(reqs) diff --git a/lib/inspec/dsl.rb b/lib/inspec/dsl.rb index 62418ccb3..c0f09a551 100644 --- a/lib/inspec/dsl.rb +++ b/lib/inspec/dsl.rb @@ -34,21 +34,17 @@ Dependencies available from this context are: EOF end - context = Inspec::ProfileContext.for_profile(dep_entry.profile, opts[:backend]) - + context = dep_entry.profile.runner_context # if we don't want all the rules, then just make 1 pass to get all rule_IDs # that we want to keep from the original filter_included_controls(context, dep_entry.profile, &block) if !opts[:include_all] - # interpret the block and skip/modify as required context.load(block) if block_given? - bind_context.add_subcontext(context) end def self.filter_included_controls(context, profile, &block) - mock = Inspec::Backend.create({ backend: 'mock' }) - include_ctx = Inspec::ProfileContext.for_profile(profile, mock) + include_ctx = profile.runner_context include_ctx.load(block) if block_given? # remove all rules that were not registered context.rules.keys.each do |id| diff --git a/lib/inspec/profile.rb b/lib/inspec/profile.rb index 99906f09d..1cf681363 100644 --- a/lib/inspec/profile.rb +++ b/lib/inspec/profile.rb @@ -8,6 +8,9 @@ require 'inspec/polyfill' require 'inspec/fetcher' require 'inspec/source_reader' require 'inspec/metadata' +require 'inspec/backend' +require 'inspec/rule' +require 'inspec/profile_context' require 'inspec/dependencies/lockfile' require 'inspec/dependencies/dependency_set' @@ -31,7 +34,7 @@ module Inspec reader end - def self.for_target(target, opts) + def self.for_target(target, opts = {}) new(resolve_target(target), opts.merge(target: target)) end @@ -47,9 +50,14 @@ module Inspec @target = @options.delete(:target) @logger = @options[:logger] || Logger.new(nil) @source_reader = source_reader + if options[:dependencies] + @locked_dependencies = options[:dependencies] + end + @controls = options[:controls] || [] @profile_id = @options[:id] - @runner_context = nil + @backend = @options[:backend] || Inspec::Backend.create(options) Metadata.finalize(@source_reader.metadata, @profile_id) + @runner_context = @options[:profile_context] || Inspec::ProfileContext.for_profile(self, @backend) end def name @@ -60,6 +68,46 @@ module Inspec @params ||= load_params end + def collect_tests(include_list = @controls) + if !@tests_collected + locked_dependencies.each(&:collect_tests) + + tests.each do |path, content| + next if content.nil? || content.empty? + abs_path = source_reader.target.abs_path(path) + @runner_context.load(content, abs_path, nil) + end + @tests_collected = true + end + filter_controls(@runner_context.all_rules, include_list) + end + + def filter_controls(controls_array, include_list) + return controls_array if include_list.nil? || include_list.empty? + controls_array.select do |c| + id = ::Inspec::Rule.rule_id(c) + include_list.include?(id) + end + end + + def load_libraries + locked_dependencies.each do |d| + c = d.load_libraries + @runner_context.add_resources(c) + end + + libs = libraries.map do |path, content| + [content, path] + end + + @runner_context.load_libraries(libs) + @runner_context + end + + def to_s + "Inspec::Profile<#{name}>" + end + def info res = params.dup # add information about the controls @@ -248,11 +296,15 @@ module Inspec # @return [Inspec::Lockfile] # def generate_lockfile(vendor_path = nil) - res = Inspec::DependencySet.new(cwd, vendor_path) + res = Inspec::DependencySet.new(cwd, vendor_path, nil, @backend) res.vendor(metadata.dependencies) Inspec::Lockfile.from_dependency_set(res) end + def load_dependencies + Inspec::DependencySet.from_lockfile(lockfile, cwd, nil, @backend) + end + private # Create an archive name for this profile and an additional options @@ -281,34 +333,17 @@ module Inspec params end - # - # Returns a new runner for the current profile. - # - # @params [Symbol] The type of backend to use when constructing a - # new runner. - # @returns [Inspec::Runner] - # - def runner_for_profile(backend = :mock) - opts = @options.dup - opts[:ignore_supports] = true - r = Runner.new(id: @profile_id, - backend: backend, - test_collector: opts.delete(:test_collector)) - r.add_profile(self, opts) - r - end - def load_checks_params(params) + load_libraries + tests = collect_tests params[:controls] = controls = {} params[:groups] = groups = {} prefix = @source_reader.target.prefix || '' - # Load from the runner_context if it exists - runner = @runner_context || runner_for_profile - runner.all_rules.each do |rule| + tests.each do |rule| f = load_rule_filepath(prefix, rule) load_rule(rule, f, controls, groups) end - params[:attributes] = runner.attributes + params[:attributes] = @runner_context.attributes params end @@ -337,9 +372,5 @@ module Inspec } groups[file][:controls].push(id) end - - def load_dependencies - Inspec::DependencySet.from_lockfile(lockfile, cwd, nil) - end end end diff --git a/lib/inspec/profile_context.rb b/lib/inspec/profile_context.rb index aca563e2d..782462b3b 100644 --- a/lib/inspec/profile_context.rb +++ b/lib/inspec/profile_context.rb @@ -12,8 +12,7 @@ require 'inspec/objects/attribute' module Inspec class ProfileContext # rubocop:disable Metrics/ClassLength def self.for_profile(profile, backend) - c = new(profile.name, backend, { 'profile' => profile }) - c.load_profile_content(profile) + new(profile.name, backend, { 'profile' => profile }) end attr_reader :attributes, :rules, :profile_id, :resource_registry @@ -28,8 +27,6 @@ module Inspec @conf = conf.dup @rules = {} @subcontexts = [] - @dependencies = {} - @dependencies = conf['profile'].locked_dependencies unless conf['profile'].nil? @require_loader = ::Inspec::RequireLoader.new @attributes = [] # A local resource registry that only contains resources defined @@ -37,6 +34,14 @@ module Inspec @resource_registry = Inspec::Resource.new_registry end + def dependencies + if @conf['profile'].nil? + {} + else + @conf['profile'].locked_dependencies + end + end + def to_resources_dsl Inspec::Resource.create_dsl(@backend, @resource_registry) end @@ -45,7 +50,7 @@ module Inspec @profile_execution_context ||= begin resources_dsl = to_resources_dsl ctx = create_context(resources_dsl) - ctx.new(@backend, @conf, @dependencies, @require_loader) + ctx.new(@backend, @conf, dependencies, @require_loader) end end @@ -59,35 +64,16 @@ module Inspec ret end - def add_subcontext(context) - Inspec::Log.debug("Adding subcontext #{context} to #{self}") + def add_resources(context) @resource_registry.merge!(context.resource_registry) + profile_execution_context.add_resources(context) reload_dsl + end + + def add_subcontext(context) @subcontexts << context end - def load_profile_content(profile) - # load_attributes_from_profile(profile) - load_libraries_from_profile(profile) - load_tests_from_profile(profile) - self - end - - def load_tests_from_profile(profile) - profile.tests.each do |path, content| - next if content.nil? || content.empty? - abs_path = profile.source_reader.target.abs_path(path) - load(content, abs_path, nil) - end - end - - def load_libraries_from_profile(profile) - libs = profile.libraries.map do |path, content| - [content, path] - end - load_libraries(libs) - end - def load_libraries(libs) lib_prefix = 'libraries' + File::SEPARATOR autoloads = [] @@ -249,14 +235,17 @@ module Inspec res end - define_method :add_subcontext do |context| - Inspec::Log.debug("Adding resource_dsl from #{context} with registry #{context.resource_registry.keys}") + define_method :add_resources do |context| self.class.class_eval do include context.to_resources_dsl end + rule_class.class_eval do include context.to_resources_dsl end + end + + define_method :add_subcontext do |context| profile_context_owner.add_subcontext(context) end diff --git a/lib/inspec/rule.rb b/lib/inspec/rule.rb index 990c0697a..fef05f6e6 100644 --- a/lib/inspec/rule.rb +++ b/lib/inspec/rule.rb @@ -33,6 +33,10 @@ module Inspec instance_eval(&block) if block_given? end + def to_s + Inspec::Rule.rule_id(self) + end + def id(*_) # never overwrite the ID @id diff --git a/lib/inspec/runner.rb b/lib/inspec/runner.rb index a094ea7f3..ee7a45c00 100644 --- a/lib/inspec/runner.rb +++ b/lib/inspec/runner.rb @@ -30,11 +30,18 @@ module Inspec # class Runner # rubocop:disable Metrics/ClassLength extend Forwardable + + def_delegator :@test_collector, :report + def_delegator :@test_collector, :reset + attr_reader :backend, :rules, :attributes def initialize(conf = {}) @rules = [] @conf = conf.dup @conf[:logger] ||= Logger.new(nil) + @target_profiles = [] + @controls = @conf[:controls] || [] + @ignore_supports = @conf[:ignore_supports] @test_collector = @conf.delete(:test_collector) || begin require 'inspec/runner_rspec' @@ -57,6 +64,26 @@ module Inspec @test_collector.backend = @backend end + def run(with = nil) + Inspec::Log.debug "Starting run with targets: #{@target_profiles.map(&:to_s)}" + Inspec::Log.debug "Backend is #{@backend}" + all_controls = [] + + @target_profiles.each do |profile| + @test_collector.add_profile(profile) + profile.locked_dependencies + profile.load_libraries + @attributes |= profile.runner_context.attributes + all_controls += profile.collect_tests + end + + all_controls.each do |rule| + register_rule(rule) + end + + @test_collector.run(with) + end + # determine all attributes before the execution, fetch data from secrets backend def load_attributes(options) attributes = {} @@ -74,8 +101,7 @@ module Inspec # # add_target allows the user to add a target whose tests will be - # run when the user calls the run method. This is the main entry - # point to profile loading. + # run when the user calls the run method. # # A target is a path or URL that points to a profile. Using this # target we generate a Profile and a ProfileContext. The content @@ -94,49 +120,15 @@ module Inspec # TODO: Deduplicate/clarify the loading code that exists in here, # the ProfileContext, the Profile, and Inspec::DSL # - # TODO: Libraries of dependent profiles should be loaded even when - # include_content/required_content aren't called. - # # @params target [String] A path or URL to a profile or raw test. - # - # @params options [Hash] An options hash. The relevant options - # considered by this call path are: - # - # - `:ignore_supports`: A boolean that controls whether to ignore - # the profile's metadata regarding supported Inspec versions or - # platforms. - # - # - `:controls`: An array of controls to run from this - # profile. Any returned rules that are not part of these - # controls will be filtered before being passed to @test_collector. + # @params _opts [Hash] Unused, but still here to avoid breaking kitchen-inspec # # @eturns [Inspec::ProfileContext] # - def add_target(target, options = {}) - profile = Inspec::Profile.for_target(target, options) + def add_target(target, _opts = []) + profile = Inspec::Profile.for_target(target, backend: @backend, controls: @controls) fail "Could not resolve #{target} to valid input." if profile.nil? - add_profile(profile, options) - end - - # Returns the profile context used to initialize this profile. - def add_profile(profile, options = {}) - return if !options[:ignore_supports] && !supports_profile?(profile) - @test_collector.add_profile(profile) - add_content_from_profile(profile, options[:controls]) - end - - def add_content_from_profile(profile, controls) - return if profile.tests.nil? || profile.tests.empty? - - ctx = Inspec::ProfileContext.for_profile(profile, @backend) - profile.runner_context = ctx - @attributes |= ctx.attributes - - filter_controls(ctx.all_rules, controls).each do |rule| - register_rule(rule) - end - - ctx + @target_profiles << profile if supports_profile?(profile) end # @@ -154,7 +146,7 @@ module Inspec end def supports_profile?(profile) - return true if profile.metadata.nil? + return true if profile.metadata.nil? || @ignore_supports if !profile.metadata.supports_runtime? fail 'This profile requires InSpec version '\ @@ -187,20 +179,8 @@ module Inspec new_tests end - def_delegator :@test_collector, :run - def_delegator :@test_collector, :report - def_delegator :@test_collector, :reset - private - def filter_controls(controls_array, include_list) - return controls_array if include_list.nil? || include_list.empty? - controls_array.select do |c| - id = ::Inspec::Rule.rule_id(c) - include_list.include?(id) - end - end - def block_source_info(block) return {} if block.nil? || !block.respond_to?(:source_location) opts = {} @@ -245,6 +225,7 @@ module Inspec end def register_rule(rule) + Inspec::Log.debug "Registering rule #{rule}" @rules << rule checks = ::Inspec::Rule.prepare_checks(rule) examples = checks.map do |m, a, b| diff --git a/test/docker_test.rb b/test/docker_test.rb index 154491b76..dde6d0cf2 100644 --- a/test/docker_test.rb +++ b/test/docker_test.rb @@ -50,7 +50,7 @@ class DockerTester puts "--> run test on docker #{container.id}" opts = { 'target' => "docker://#{container.id}" } runner = Inspec::Runner.new(opts) - @tests.each { |test| runner.add_target(test, opts) } + @tests.each { |test| runner.add_target(test) } runner.tests.map { |g| g.run(report) } end end diff --git a/test/helper.rb b/test/helper.rb index 91a99a54b..9967233b8 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -292,6 +292,7 @@ class MockLoader def self.load_profile(name, opts = {}) opts[:test_collector] = Inspec::RunnerMock.new + opts[:backend] = Inspec::Backend.create(opts) Inspec::Profile.for_target(profile_path(name), opts) end diff --git a/test/unit/dsl/control_test.rb b/test/unit/dsl/control_test.rb index 7556f77e5..2d8c9640c 100644 --- a/test/unit/dsl/control_test.rb +++ b/test/unit/dsl/control_test.rb @@ -10,7 +10,7 @@ describe 'controls' do 'inspec.yml' => "name: mock", 'controls/mock.rb' => "control '1' do\n#{content}\nend\n", } - opts = { test_collector: Inspec::RunnerMock.new } + opts = { test_collector: Inspec::RunnerMock.new, backend: Inspec::Backend.create({}) } Inspec::Profile.for_target(data, opts) .params[:controls]['1'] end diff --git a/test/unit/mock/profiles/dependencies/profile_b/controls/example.rb b/test/unit/mock/profiles/dependencies/profile_b/controls/example.rb index e7e6dec8d..8c41170cb 100644 --- a/test/unit/mock/profiles/dependencies/profile_b/controls/example.rb +++ b/test/unit/mock/profiles/dependencies/profile_b/controls/example.rb @@ -17,6 +17,6 @@ end control 'profileb-2' do describe gordon_config do - its('version') { should eq('1.0') } + its('version') { should eq('2.0') } end end diff --git a/test/unit/mock/profiles/dependencies/profile_c/libraries/gordon_config.rb b/test/unit/mock/profiles/dependencies/profile_c/libraries/gordon_config.rb index fc450914b..cecf89512 100644 --- a/test/unit/mock/profiles/dependencies/profile_c/libraries/gordon_config.rb +++ b/test/unit/mock/profiles/dependencies/profile_c/libraries/gordon_config.rb @@ -1,54 +1,15 @@ -require 'yaml' -puts "Loading gordon_config from c" - -# Custom resource based on the InSpec resource DSL class GordonConfig < Inspec.resource(1) name 'gordon_config' - desc " - Gordon's resource description ... - " + desc "Gordon's resource description ..." example " describe gordon_config do its('version') { should eq('1.0') } - its('file_size') { should > 1 } end " - # Load the configuration file on initialization - def initialize - @params = {} - @path = '/tmp/gordon/config.yaml' - @file = inspec.file(@path) - return skip_resource "Can't find file \"#{@path}\"" if !@file.file? - - # Protect from invalid YAML content - begin - @params = YAML.load(@file.content) - # Add two extra matchers - @params['file_size'] = @file.size - @params['file_path'] = @path - @params['ruby'] = 'RUBY IS HERE TO HELP ME!' - rescue Exception - return skip_resource "#{@file}: #{$!}" - end - end - - # Example method called by 'it { should exist }' - # Returns true or false from the 'File.exists?' method - def exists? - return File.exists?(@path) - end - - # Example matcher for the number of commas in the file - def comma_count - text = @file.content - return text.count(',') - end - - # Expose all parameters - def method_missing(name) - return @params[name.to_s] + def version + "1.0" end end diff --git a/test/unit/mock/profiles/dependencies/profile_d/libraries/gordon_config.rb b/test/unit/mock/profiles/dependencies/profile_d/libraries/gordon_config.rb index 3d69a935d..8b033559f 100644 --- a/test/unit/mock/profiles/dependencies/profile_d/libraries/gordon_config.rb +++ b/test/unit/mock/profiles/dependencies/profile_d/libraries/gordon_config.rb @@ -1,54 +1,15 @@ -require 'yaml' -puts "Loading gordon_config from d" - -# Custom resource based on the InSpec resource DSL class GordonConfig < Inspec.resource(1) name 'gordon_config' - desc " - Gordon's resource description ... - " + desc "Gordon's resource description ..." example " describe gordon_config do - its('version') { should eq('1.0') } - its('file_size') { should > 1 } + its('version') { should eq('2.0') } end " - # Load the configuration file on initialization - def initialize - @params = {} - @path = '/tmp/gordon/config2.yaml' - @file = inspec.file(@path) - return skip_resource "Can't find file \"#{@path}\"" if !@file.file? - - # Protect from invalid YAML content - begin - @params = YAML.load(@file.content) - # Add two extra matchers - @params['file_size'] = @file.size - @params['file_path'] = @path - @params['ruby'] = 'RUBY IS HERE TO HELP ME!' - rescue Exception - return skip_resource "#{@file}: #{$!}" - end - end - - # Example method called by 'it { should exist }' - # Returns true or false from the 'File.exists?' method - def exists? - return File.exists?(@path) - end - - # Example matcher for the number of commas in the file - def comma_count - text = @file.content - return text.count(',') - end - - # Expose all parameters - def method_missing(name) - return @params[name.to_s] + def version + "2.0" end end