diff --git a/acceptance/lib/puppet/acceptance/environment_utils.rb b/acceptance/lib/puppet/acceptance/environment_utils.rb index 28ff29089..2df232b80 100644 --- a/acceptance/lib/puppet/acceptance/environment_utils.rb +++ b/acceptance/lib/puppet/acceptance/environment_utils.rb @@ -1,354 +1,354 @@ require 'puppet/acceptance/module_utils' module Puppet module Acceptance module EnvironmentUtils include Puppet::Acceptance::ModuleUtils # Generate puppet manifest for the creation of an environment with # the given modulepath and manifest and env_name. The created environment # will have on testing_mod module, and manifest site.pp which includes it. # # @param options [Hash] # @option options [String] :modulepath Modules directory # @option options [String] :manifest Manifest directory # @option options [String] :env_name Environment name # @return [String] Puppet manifest to create the environment files def generate_environment(options) modulepath = options[:modulepath] manifestpath = options[:manifestpath] env_name = options[:env_name] environment = <<-MANIFEST_SNIPPET file { ################################################### # #{env_name} #{generate_module("testing_mod", env_name, modulepath)} "#{manifestpath}":; "#{manifestpath}/site.pp": ensure => file, mode => "0640", content => ' notify { "in #{env_name} site.pp": } include testing_mod ' ; } MANIFEST_SNIPPET end # Generate one module's manifest code. def generate_module(module_name, env_name, modulepath) module_pp = <<-MANIFEST_SNIPPET "#{modulepath}":; "#{modulepath}/#{module_name}":; "#{modulepath}/#{module_name}/manifests":; "#{modulepath}/#{module_name}/manifests/init.pp": ensure => file, mode => "0640", content => 'class #{module_name} { notify { "include #{env_name} #{module_name}": } }' ; MANIFEST_SNIPPET end # Default, legacy, dynamic and directory environments # using generate_manifest(), all rooted in testdir. # # @param [String] testdir path to the temp directory which will be the confdir all # the environments live in # @return [String] Puppet manifest to generate all of the environment files. def environment_manifest(testdir) manifest = <<-MANIFEST File { ensure => directory, owner => #{master['user']}, group => #{master['group']}, mode => "0750", } file { "#{testdir}": } #{generate_environment( :modulepath => "#{testdir}/modules", :manifestpath => "#{testdir}/manifests", :env_name => "default environment")} #{generate_environment( :modulepath => "#{testdir}/testing-modules", :manifestpath => "#{testdir}/testing-manifests", :env_name => "legacy testing environment")} file { "#{testdir}/dynamic":; "#{testdir}/dynamic/testing":; } #{generate_environment( :modulepath => "#{testdir}/dynamic/testing/modules", :manifestpath => "#{testdir}/dynamic/testing/manifests", :env_name => "dynamic testing environment")} file { "#{testdir}/environments":; "#{testdir}/environments/testing":; } #{generate_environment( :modulepath => "#{testdir}/environments/testing/modules", :manifestpath => "#{testdir}/environments/testing/manifests", :env_name => "directory testing environment")} file { "#{testdir}/environments/testing_environment_conf":; } #{generate_environment( :modulepath => "#{testdir}/environments/testing_environment_conf/nonstandard-modules", :manifestpath => "#{testdir}/environments/testing_environment_conf/nonstandard-manifests", :env_name => "directory testing with environment.conf")} file { "#{testdir}/environments/testing_environment_conf/environment.conf": ensure => file, mode => "0640", content => ' modulepath = nonstandard-modules:$basemodulepath manifest = nonstandard-manifests config_version = local-version.sh ' } file { "#{testdir}/environments/testing_environment_conf/local-version.sh": ensure => file, mode => "0640", content => '#! /usr/bin/env bash echo "local testing_environment_conf"' ; } ################### # Services file { "#{testdir}/services":; "#{testdir}/services/testing":; #{generate_module('service_mod', "service testing environment", "#{testdir}/services/testing/modules")} } ####################### # Config version script file { "#{testdir}/static-version.sh": ensure => file, mode => "0640", content => '#! /usr/bin/env bash echo "static"' ; } MANIFEST end def get_directory_hash_from(host, path) dir_hash = {} on(host, "ls #{path}") do |result| result.stdout.split.inject(dir_hash) do |hash,f| hash[f] = "#{path}/#{f}" hash end end dir_hash end def safely_shadow_directory_contents_and_yield(host, original_path, new_path, &block) original_files = get_directory_hash_from(host, original_path) new_files = get_directory_hash_from(host, new_path) conflicts = original_files.keys & new_files.keys step "backup original files" do conflicts.each do |c| on(host, "mv #{original_files[c]} #{original_files[c]}.bak") end end step "shadow original files with temporary files" do new_files.each do |name,full_path_name| on(host, "cp -R #{full_path_name} #{original_path}/#{name}") end end - step "open permissions to 770 on all temporary files copied into working dir and set ownership" do + step "open permissions to 755 on all temporary files copied into working dir and set ownership" do file_list = new_files.keys.map { |name| "#{original_path}/#{name}" }.join(' ') - on(host, "chown -R #{host['user']}:#{host['group']} #{file_list}") - on(host, "chmod -R 770 #{file_list}") + on(host, "chown -R #{host.puppet['user']}:#{host.puppet['group']} #{file_list}") + on(host, "chmod -R 755 #{file_list}") end yield ensure step "clear out the temporary files" do files_to_delete = new_files.keys.map { |name| "#{original_path}/#{name}" } on(host, "rm -rf #{files_to_delete.join(' ')}") end step "move the shadowed files back to their original places" do conflicts.each do |c| on(host, "mv #{original_files[c]}.bak #{original_files[c]}") end end end # Stand up a puppet master on the master node with the given master_opts # using the passed envdir as the source of the puppet environment files, # and passed confdir as the directory to use for the temporary # puppet.conf. It then runs through a series of environment tests for the # passed environment and returns a hashed structure of the results. # # @return [Hash>] Hash of # Beaker::Hosts for each agent run keyed to a hash of Beaker::Result # objects keyed by each subtest that was performed. def use_an_environment(environment, description, master_opts, envdir, confdir, options = {}) master_puppet_conf = master_opts.dup # shallow clone results = {} safely_shadow_directory_contents_and_yield(master, master['puppetpath'], envdir) do config_print = options[:config_print] directory_environments = options[:directory_environments] with_puppet_running_on(master, master_puppet_conf, confdir) do agents.each do |agent| agent_results = results[agent] = {} step "puppet agent using #{description} environment" args = "-t", "--server", master args << ["--environment", environment] if environment # Test agents configured to use directory environments (affects environment # loading on the agent, especially with regards to requests/node environment) args << "--environmentpath='$confdir/environments'" if directory_environments && agent != master on(agent, puppet("agent", *args), :acceptable_exit_codes => (0..255)) do agent_results[:puppet_agent] = result end if agent == master args = ["--trace"] args << ["--environment", environment] if environment step "print puppet config for #{description} environment" on(agent, puppet(*(["config", "print", "basemodulepath", "modulepath", "manifest", "config_version", config_print] + args)), :acceptable_exit_codes => (0..255)) do agent_results[:puppet_config] = result end step "puppet apply using #{description} environment" on(agent, puppet(*(["apply", '-e', '"include testing_mod"'] + args)), :acceptable_exit_codes => (0..255)) do agent_results[:puppet_apply] = result end # Be aware that Puppet Module Tool will create the module directory path if it # does not exist. So these tests should be run last... step "install a module into environment" on(agent, puppet(*(["module", "install", "pmtacceptance-nginx"] + args)), :acceptable_exit_codes => (0..255)) do agent_results[:puppet_module_install] = result end step "uninstall a module from #{description} environment" on(agent, puppet(*(["module", "uninstall", "pmtacceptance-nginx"] + args)), :acceptable_exit_codes => (0..255)) do agent_results[:puppet_module_uninstall] = result end end end end end return results end # For each Beaker::Host in the results Hash, generates a chart, comparing # the expected exit code and regexp matches from expectations to the # Beaker::Result.output for a particular command that was executed in the # environment. Outputs either 'ok' or text highlighting the errors, and # returns false if any errors were found. # # @param [Hash>] results # @param [Hash Integer,Array}>] expectations # @return [Array] Returns an empty array of there were no failures, or an # Array of failed cases. def review_results(results, expectations) failed = [] results.each do |agent, agent_results| divider = "-" * 79 logger.info divider logger.info "For: (#{agent.name}) #{agent}" logger.info divider agent_results.each do |testname, execution_results| expected_exit_code = expectations[testname][:exit_code] match_tests = expectations[testname][:matches] || [] not_match_tests = expectations[testname][:does_not_match] || [] expect_failure = expectations[testname][:expect_failure] notes = expectations[testname][:notes] errors = [] if execution_results.exit_code != expected_exit_code errors << "To exit with an exit code of '#{expected_exit_code}', instead of '#{execution_results.exit_code}'" end match_tests.each do |regexp| if execution_results.output !~ regexp errors << "#{errors.empty? ? "To" : "And"} match: #{regexp}" end end not_match_tests.each do |regexp| if execution_results.output =~ regexp errors << "#{errors.empty? ? "Not to" : "And not"} match: #{regexp}" end end error_msg = "Expected the output:\n#{execution_results.output}\n#{errors.join("\n")}" unless errors.empty? case_failed = case when errors.empty? && expect_failure then 'ok - failed as expected' when errors.empty? && !expect_failure then 'ok' else '*UNEXPECTED FAILURE*' end logger.info "#{testname}: #{case_failed}" if case_failed == 'ok - failed as expected' logger.info divider logger.info "Case is known to fail as follows:\n#{execution_results.output}\n" elsif case_failed == '*UNEXPECTED FAILURE*' failed << "Unexpected failure for #{testname}" logger.info divider logger.info "#{error_msg}" end logger.info("------\nNotes: #{notes}") if notes logger.info divider end end return failed end def assert_review(review) failures = [] review.each do |scenario, failed| if !failed.empty? problems = "Problems in the '#{scenario}' output reported above:\n #{failed.join("\n ")}" logger.warn(problems) failures << problems end end assert failures.empty?, "Failed Review:\n\n#{failures.join("\n")}\n" end end end end diff --git a/acceptance/tests/agent/agent_disable_lockfile.rb b/acceptance/tests/agent/agent_disable_lockfile.rb index 0fcc5bfd5..e93f18e05 100644 --- a/acceptance/tests/agent/agent_disable_lockfile.rb +++ b/acceptance/tests/agent/agent_disable_lockfile.rb @@ -1,94 +1,97 @@ test_name "the agent --disable/--enable functionality should manage the agent lockfile properly" # # This test is intended to ensure that puppet agent --enable/--disable # work properly, both in terms of complying with our public "API" around # lockfile semantics ( http://links.puppetlabs.com/agent_lockfiles ), and # in terms of actually restricting or allowing new agent runs to begin. # require 'puppet/acceptance/temp_file_utils' extend Puppet::Acceptance::TempFileUtils initialize_temp_dirs() @all_tests_passed = false ############################################################################### # BEGIN TEST LOGIC ############################################################################### teardown do if @all_tests_passed then remove_temp_dirs() end + agents.each do |agent| + on(agent, puppet('agent', "--enable")) + end end tuples = [ ["reason not specified", false], ["I'm busy; go away.'", true] ] tuples.each do |expected_message, explicitly_specify_message| with_puppet_running_on(master, {}) do step "disable the agent; specify message? '#{explicitly_specify_message}', message: '#{expected_message}'" do agents.each do |agent| if (explicitly_specify_message) on(agent, puppet('agent', "--disable \"#{expected_message}\"")) else on(agent, puppet('agent', "--disable")) end agent_disabled_lockfile = "#{agent.puppet['vardir']}/state/agent_disabled.lock" unless file_exists?(agent, agent_disabled_lockfile) then fail_test("Failed to create disabled lock file '#{agent_disabled_lockfile}' on agent '#{agent}'") end lock_file_content = file_contents(agent, agent_disabled_lockfile) # This is a hack; we should parse the JSON into a hash, but I don't # think I have a library available from the acceptance test framework # that I can use to do that. So I'm falling back to regex. lock_file_content_regex = /"disabled_message"\s*:\s*"#{expected_message}"/ unless lock_file_content =~ lock_file_content_regex fail_test("Disabled lock file contents invalid; expected to match '#{lock_file_content_regex}', got '#{lock_file_content}' on agent '#{agent}'") end end end step "attempt to run the agent (message: '#{expected_message}')" do agents.each do |agent| on(agent, puppet('agent', "--test --server #{master}"), :acceptable_exit_codes => [1]) do disabled_regex = /administratively disabled.*'#{expected_message}'/ unless result.stdout =~ disabled_regex fail_test("Unexpected output from attempt to run agent disabled; expecting to match '#{disabled_regex}', got '#{result.stdout}' on agent '#{agent}'") end end end end step "enable the agent (message: '#{expected_message}')" do agents.each do |agent| agent_disabled_lockfile = "#{agent.puppet['vardir']}/state/agent_disabled.lock" on(agent, puppet('agent', "--enable")) if file_exists?(agent, agent_disabled_lockfile) then fail_test("Failed to remove disabled lock file '#{agent_disabled_lockfile}' on agent '#{agent}'") end end end step "verify that we can run the agent (message: '#{expected_message}')" do agents.each do |agent| on(agent, puppet('agent', "--test --server #{master}")) end end end # with_puppet_running_on block end # tuples block @all_tests_passed = true diff --git a/lib/puppet/environments.rb b/lib/puppet/environments.rb index 1095759fd..ec7a12940 100644 --- a/lib/puppet/environments.rb +++ b/lib/puppet/environments.rb @@ -1,409 +1,411 @@ # @api private module Puppet::Environments class EnvironmentNotFound < Puppet::Error def initialize(environment_name, original = nil) environmentpath = Puppet[:environmentpath] super("Could not find a directory environment named '#{environment_name}' anywhere in the path: #{environmentpath}. Does the directory exist?", original) end end # @api private module EnvironmentCreator # Create an anonymous environment. # # @param module_path [String] A list of module directories separated by the # PATH_SEPARATOR # @param manifest [String] The path to the manifest # @return A new environment with the `name` `:anonymous` # # @api private def for(module_path, manifest) Puppet::Node::Environment.create(:anonymous, module_path.split(File::PATH_SEPARATOR), manifest) end end # @!macro [new] loader_search_paths # A list of indicators of where the loader is getting its environments from. # @return [Array] The URIs of the load locations # # @!macro [new] loader_list # @return [Array] All of the environments known # to the loader # # @!macro [new] loader_get # Find a named environment # # @param name [String,Symbol] The name of environment to find # @return [Puppet::Node::Environment, nil] the requested environment or nil # if it wasn't found # # @!macro [new] loader_get_conf # Attempt to obtain the initial configuration for the environment. Not all # loaders can provide this. # # @param name [String,Symbol] The name of the environment whose configuration # we are looking up # @return [Puppet::Setting::EnvironmentConf, nil] the configuration for the # requested environment, or nil if not found or no configuration is available # # @!macro [new] loader_get_or_fail # Find a named environment or raise # Puppet::Environments::EnvironmentNotFound when the named environment is # does not exist. # # @param name [String,Symbol] The name of environment to find # @return [Puppet::Node::Environment] the requested environment # A source of pre-defined environments. # # @api private class Static include EnvironmentCreator def initialize(*environments) @environments = environments end # @!macro loader_search_paths def search_paths ["data:text/plain,internal"] end # @!macro loader_list def list @environments end # @!macro loader_get def get(name) @environments.find do |env| env.name == name.intern end end # @!macro loader_get_or_fail def get!(name) if !environment = get(name) raise EnvironmentNotFound, name end environment end # Returns a basic environment configuration object tied to the environment's # implementation values. Will not interpolate. # # @!macro loader_get_conf def get_conf(name) env = get(name) if env Puppet::Settings::EnvironmentConf.static_for(env) else nil end end end # A source of unlisted pre-defined environments. # # Used only for internal bootstrapping environments which are not relevant # to an end user (such as the fall back 'configured' environment). # # @api private class StaticPrivate < Static # Unlisted # # @!macro loader_list def list [] end end # Old-style environments that come either from explicit stanzas in # puppet.conf or from dynamic environments created from use of `$environment` # in puppet.conf. # # @example Explicit Stanza # [environment_name] # modulepath=/var/my_env/modules # # @example Dynamic Environments # [master] # modulepath=/var/$environment/modules # # @api private class Legacy include EnvironmentCreator # @!macro loader_search_paths def search_paths ["file://#{Puppet[:config]}"] end # @note The list of environments for the Legacy environments is always # empty. # # @!macro loader_list def list [] end # @note Because the Legacy system cannot list out all of its environments, # get is able to return environments that are not returned by a call to # {#list}. # # @!macro loader_get def get(name) Puppet::Node::Environment.new(name) end # @note Because the Legacy system cannot list out all of its environments, # this method will never fail and is only calling get directly. # # @!macro loader_get_or_fail def get!(name) get(name) end # @note we could return something here, but since legacy environments # are deprecated, there is no point. # # @!macro loader_get_conf def get_conf(name) nil end end # Reads environments from a directory on disk. Each environment is # represented as a sub-directory. The environment's manifest setting is the # `manifest` directory of the environment directory. The environment's # modulepath setting is the global modulepath (from the `[master]` section # for the master) prepended with the `modules` directory of the environment # directory. # # @api private class Directories def initialize(environment_dir, global_module_path) @environment_dir = environment_dir @global_module_path = global_module_path end # Generate an array of directory loaders from a path string. # @param path [String] path to environment directories # @param global_module_path [Array] the global modulepath setting # @return [Array] An array # of configured directory loaders. def self.from_path(path, global_module_path) environments = path.split(File::PATH_SEPARATOR) environments.map do |dir| Puppet::Environments::Directories.new(dir, global_module_path) end end # @!macro loader_search_paths def search_paths ["file://#{@environment_dir}"] end # @!macro loader_list def list valid_directories.collect do |envdir| - name = Puppet::FileSystem.basename_string(envdir) + name = Puppet::FileSystem.basename_string(envdir).intern setting_values = Puppet.settings.values(name, Puppet.settings.preferred_run_mode) env = Puppet::Node::Environment.create( - name.intern, + name, Puppet::Node::Environment.split_path(setting_values.interpolate(:modulepath)), setting_values.interpolate(:manifest), setting_values.interpolate(:config_version) ) env.watching = false env end end # @!macro loader_get def get(name) list.find { |env| env.name == name.intern } end # @!macro loader_get_or_fail def get!(name) if !environment = get(name) raise EnvironmentNotFound, name end environment end # @!macro loader_get_conf def get_conf(name) valid_directories.each do |envdir| envname = Puppet::FileSystem.basename_string(envdir) if envname == name.to_s return Puppet::Settings::EnvironmentConf.load_from(envdir, @global_module_path) end end nil end private def valid_directories if Puppet::FileSystem.directory?(@environment_dir) Puppet::FileSystem.children(@environment_dir).select do |child| name = Puppet::FileSystem.basename_string(child) Puppet::FileSystem.directory?(child) && Puppet::Node::Environment.valid_name?(name) end else [] end end end # Combine together multiple loaders to act as one. # @api private class Combined def initialize(*loaders) @loaders = loaders end # @!macro loader_search_paths def search_paths @loaders.collect(&:search_paths).flatten end # @!macro loader_list def list @loaders.collect(&:list).flatten end # @!macro loader_get def get(name) @loaders.each do |loader| if env = loader.get(name) return env end end nil end # @!macro loader_get_or_fail def get!(name) @loaders.each do |loader| if env = loader.get(name) return env end end raise EnvironmentNotFound, name end # @!macro loader_get_conf def get_conf(name) @loaders.each do |loader| if conf = loader.get_conf(name) return conf end end nil end end class Cached < Combined INFINITY = 1.0 / 0.0 def initialize(*loaders) super @cache = {} end def get(name) evict_if_expired(name) if result = @cache[name] return result.value elsif (result = super(name)) @cache[name] = entry(result) result end end # Clears the cache of the environment with the given name. # (The intention is that this could be used from a MANUAL cache eviction command (TBD) def clear(name) @cache.delete(name) end # Clears all cached environments. # (The intention is that this could be used from a MANUAL cache eviction command (TBD) def clear_all() @cache = {} end # This implementation evicts the cache, and always gets the current configuration of the environment # TODO: While this is wasteful since it needs to go on a search for the conf, it is too disruptive to optimize # this. # def get_conf(name) evict_if_expired(name) super name end # Creates a suitable cache entry given the time to live for one environment # def entry(env) ttl = (conf = get_conf(env.name)) ? conf.environment_timeout : Puppet.settings.value(:environment_timeout) case ttl when 0 NotCachedEntry.new(env) # Entry that is always expired (avoids syscall to get time) when INFINITY Entry.new(env) # Entry that never expires (avoids syscall to get time) else TTLEntry.new(env, ttl) end end # Evicts the entry if it has expired - # + # Also clears caches in Settings that may prevent the entry from being updated def evict_if_expired(name) if (result = @cache[name]) && result.expired? @cache.delete(name) + + Puppet.settings.clear_environment_settings(name) end end # Never evicting entry class Entry attr_reader :value def initialize(value) @value = value end def expired? false end end # Always evicting entry class NotCachedEntry < Entry def expired? true end end # Time to Live eviction policy entry class TTLEntry < Entry def initialize(value, ttl_seconds) super value @ttl = Time.now + ttl_seconds end def expired? Time.now > @ttl end end end end diff --git a/lib/puppet/parser/ast/pops_bridge.rb b/lib/puppet/parser/ast/pops_bridge.rb index 9c6a31744..22a687971 100644 --- a/lib/puppet/parser/ast/pops_bridge.rb +++ b/lib/puppet/parser/ast/pops_bridge.rb @@ -1,252 +1,242 @@ require 'puppet/parser/ast/top_level_construct' require 'puppet/pops' # The AST::Bridge contains classes that bridges between the new Pops based model # and the 3.x AST. This is required to be able to reuse the Puppet::Resource::Type which is # fundamental for the rest of the logic. # class Puppet::Parser::AST::PopsBridge # Bridges to one Pops Model Expression # The @value is the expression # This is used to represent the body of a class, definition, or node, and for each parameter's default value # expression. # class Expression < Puppet::Parser::AST::Leaf def initialize args super @@evaluator ||= Puppet::Pops::Parser::EvaluatingParser.new() end def to_s Puppet::Pops::Model::ModelTreeDumper.new.dump(@value) end def evaluate(scope) @@evaluator.evaluate(scope, @value) end # Adapts to 3x where top level constructs needs to have each to iterate over children. Short circuit this # by yielding self. By adding this there is no need to wrap a pops expression inside an AST::BlockExpression # def each yield self end def sequence_with(other) if value.nil? # This happens when testing and not having a complete setup other else # When does this happen ? Ever ? raise "sequence_with called on Puppet::Parser::AST::PopsBridge::Expression - please report use case" # What should be done if the above happens (We don't want this to happen). # Puppet::Parser::AST::BlockExpression.new(:children => [self] + other.children) end end # The 3x requires code plugged in to an AST to have this in certain positions in the tree. The purpose # is to either print the content, or to look for things that needs to be defined. This implementation # cheats by always returning an empty array. (This allows simple files to not require a "Program" at the top. # def children [] end end - class NilAsUndefExpression < Expression - def evaluate(scope) - result = super - result.nil? ? :undef : result - end - end - # Bridges the top level "Program" produced by the pops parser. # Its main purpose is to give one point where all definitions are instantiated (actually defined since the # Puppet 3x terminology is somewhat misleading - the definitions are instantiated, but instances of the created types # are not created, that happens when classes are included / required, nodes are matched and when resources are instantiated # by a resource expression (which is also used to instantiate a host class). # class Program < Puppet::Parser::AST::TopLevelConstruct attr_reader :program_model, :context def initialize(program_model, context = {}) @program_model = program_model @context = context @ast_transformer ||= Puppet::Pops::Model::AstTransformer.new(@context[:file]) @@evaluator ||= Puppet::Pops::Parser::EvaluatingParser.new() end # This is the 3x API, the 3x AST searches through all code to find the instructions that can be instantiated. # This Pops-model based instantiation relies on the parser to build this list while parsing (which is more # efficient as it avoids one full scan of all logic via recursive enumeration/yield) # def instantiate(modname) @program_model.definitions.collect do |d| case d when Puppet::Pops::Model::HostClassDefinition instantiate_HostClassDefinition(d, modname) when Puppet::Pops::Model::ResourceTypeDefinition instantiate_ResourceTypeDefinition(d, modname) when Puppet::Pops::Model::NodeDefinition instantiate_NodeDefinition(d, modname) else raise Puppet::ParseError, "Internal Error: Unknown type of definition - got '#{d.class}'" end end.flatten().compact() # flatten since node definition may have returned an array # Compact since functions are not understood by compiler end def evaluate(scope) @@evaluator.evaluate(scope, program_model) end # Adapts to 3x where top level constructs needs to have each to iterate over children. Short circuit this # by yielding self. This means that the HostClass container will call this bridge instance with `instantiate`. # def each yield self end private def instantiate_Parameter(o) # 3x needs parameters as an array of `[name]` or `[name, value_expr]` - # One problem is that the parameter evaluation takes place in the wrong context in 3x (the caller's and - # can thus reference all sorts of information. Here the value expression is wrapped in an AST Bridge to a Pops - # expression since the Pops side can not control the evaluation if o.value - [o.name, NilAsUndefExpression.new(:value => o.value)] + [o.name, Expression.new(:value => o.value)] else [o.name] end end def create_type_map(definition) result = {} # No need to do anything if there are no parameters return result unless definition.parameters.size > 0 # No need to do anything if there are no typed parameters typed_parameters = definition.parameters.select {|p| p.type_expr } return result if typed_parameters.empty? # If there are typed parameters, they need to be evaluated to produce the corresponding type # instances. This evaluation requires a scope. A scope is not available when doing deserialization # (there is also no initialized evaluator). When running apply and test however, the environment is # reused and we may reenter without a scope (which is fine). A debug message is then output in case # there is the need to track down the odd corner case. See {#obtain_scope}. # if scope = obtain_scope typed_parameters.each do |p| result[p.name] = @@evaluator.evaluate(scope, p.type_expr) end end result end # Obtains the scope or issues a warning if :global_scope is not bound def obtain_scope scope = Puppet.lookup(:global_scope) do # This occurs when testing and when applying a catalog (there is no scope available then), and # when running tests that run a partial setup. # This is bad if the logic is trying to compile, but a warning can not be issues since it is a normal # use case that there is no scope when requesting the type in order to just get the parameters. Puppet.debug("Instantiating Resource with type checked parameters - scope is missing, skipping type checking.") nil end scope end # Produces a hash with data for Definition and HostClass def args_from_definition(o, modname) args = { :arguments => o.parameters.collect {|p| instantiate_Parameter(p) }, :argument_types => create_type_map(o), :module_name => modname } unless is_nop?(o.body) args[:code] = Expression.new(:value => o.body) end @ast_transformer.merge_location(args, o) end def instantiate_HostClassDefinition(o, modname) args = args_from_definition(o, modname) args[:parent] = absolute_reference(o.parent_class) Puppet::Resource::Type.new(:hostclass, o.name, @context.merge(args)) end def instantiate_ResourceTypeDefinition(o, modname) Puppet::Resource::Type.new(:definition, o.name, @context.merge(args_from_definition(o, modname))) end def instantiate_NodeDefinition(o, modname) args = { :module_name => modname } unless is_nop?(o.body) args[:code] = Expression.new(:value => o.body) end unless is_nop?(o.parent) args[:parent] = @ast_transformer.hostname(o.parent) end host_matches = @ast_transformer.hostname(o.host_matches) @ast_transformer.merge_location(args, o) host_matches.collect do |name| Puppet::Resource::Type.new(:node, name, @context.merge(args)) end end # Propagates a found Function to the appropriate loader. # This is for 4x future-evaluator/loader # def instantiate_FunctionDefinition(function_definition, modname) loaders = (Puppet.lookup(:loaders) { nil }) unless loaders raise Puppet::ParseError, "Internal Error: Puppet Context ':loaders' missing - cannot define any functions" end loader = if modname.nil? || modname == "" # TODO : Later when functions can be private, a decision is needed regarding what that means. # A private environment loader could be used for logic outside of modules, then only that logic # would see the function. # # Use the private loader, this function may see the environment's dependencies (currently, all modules) loaders.private_environment_loader() else # TODO : Later check if function is private, and then add it to # private_loader_for_module # loaders.public_loader_for_module(modname) end unless loader raise Puppet::ParseError, "Internal Error: did not find public loader for module: '#{modname}'" end # Instantiate Function, and store it in the environment loader typed_name, f = Puppet::Pops::Loader::PuppetFunctionInstantiator.create_from_model(function_definition, loader) loader.set_entry(typed_name, f, Puppet::Pops::Adapters::SourcePosAdapter.adapt(function_definition).to_uri) nil # do not want the function to inadvertently leak into 3x end def code() Expression.new(:value => @value) end def is_nop?(o) @ast_transformer.is_nop?(o) end def absolute_reference(ref) if ref.nil? || ref.empty? || ref.start_with?('::') ref else "::#{ref}" end end end end diff --git a/lib/puppet/parser/resource.rb b/lib/puppet/parser/resource.rb index 5dac00cca..976b4fa19 100644 --- a/lib/puppet/parser/resource.rb +++ b/lib/puppet/parser/resource.rb @@ -1,269 +1,267 @@ require 'puppet/resource' # The primary difference between this class and its # parent is that this class has rules on who can set # parameters class Puppet::Parser::Resource < Puppet::Resource require 'puppet/parser/resource/param' require 'puppet/util/tagging' require 'puppet/parser/yaml_trimmer' require 'puppet/resource/type_collection_helper' include Puppet::Resource::TypeCollectionHelper include Puppet::Util include Puppet::Util::MethodHelper include Puppet::Util::Errors include Puppet::Util::Logging include Puppet::Parser::YamlTrimmer attr_accessor :source, :scope, :collector_id attr_accessor :virtual, :override, :translated, :catalog, :evaluated attr_accessor :file, :line attr_reader :exported, :parameters # Determine whether the provided parameter name is a relationship parameter. def self.relationship_parameter?(name) @relationship_names ||= Puppet::Type.relationship_params.collect { |p| p.name } @relationship_names.include?(name) end # Set up some boolean test methods def translated?; !!@translated; end def override?; !!@override; end def evaluated?; !!@evaluated; end def [](param) param = param.intern if param == :title return self.title end if @parameters.has_key?(param) @parameters[param].value else nil end end def eachparam @parameters.each do |name, param| yield param end end def environment scope.environment end # Process the stage metaparameter for a class. A containment edge # is drawn from the class to the stage. The stage for containment # defaults to main, if none is specified. def add_edge_to_stage return unless self.class? unless stage = catalog.resource(:stage, self[:stage] || (scope && scope.resource && scope.resource[:stage]) || :main) raise ArgumentError, "Could not find stage #{self[:stage] || :main} specified by #{self}" end self[:stage] ||= stage.title unless stage.title == :main catalog.add_edge(stage, self) end # Retrieve the associated definition and evaluate it. def evaluate return if evaluated? @evaluated = true if klass = resource_type and ! builtin_type? finish evaluated_code = klass.evaluate_code(self) return evaluated_code elsif builtin? devfail "Cannot evaluate a builtin type (#{type})" else self.fail "Cannot find definition #{type}" end end # Mark this resource as both exported and virtual, # or remove the exported mark. def exported=(value) if value @virtual = true @exported = value else @exported = value end end # Do any finishing work on this object, called before evaluation or # before storage/translation. def finish return if finished? @finished = true add_defaults add_scope_tags validate end # Has this resource already been finished? def finished? @finished end def initialize(*args) raise ArgumentError, "Resources require a hash as last argument" unless args.last.is_a? Hash raise ArgumentError, "Resources require a scope" unless args.last[:scope] super @source ||= scope.source end # Is this resource modeling an isomorphic resource type? def isomorphic? if builtin_type? return resource_type.isomorphic? else return true end end # Merge an override resource in. This will throw exceptions if # any overrides aren't allowed. def merge(resource) # Test the resource scope, to make sure the resource is even allowed # to override. unless self.source.object_id == resource.source.object_id || resource.source.child_of?(self.source) raise Puppet::ParseError.new("Only subclasses can override parameters", resource.line, resource.file) end # Some of these might fail, but they'll fail in the way we want. resource.parameters.each do |name, param| override_parameter(param) end end def name self[:name] || self.title end # A temporary occasion, until I get paths in the scopes figured out. alias path to_s # Define a parameter in our resource. # if we ever receive a parameter named 'tag', set # the resource tags with its value. def set_parameter(param, value = nil) - if ! value.nil? + if ! param.is_a?(Puppet::Parser::Resource::Param) param = Puppet::Parser::Resource::Param.new( :name => param, :value => value, :source => self.source ) - elsif ! param.is_a?(Puppet::Parser::Resource::Param) - raise ArgumentError, "Received incomplete information - no value provided for parameter #{param}" end tag(*param.value) if param.name == :tag # And store it in our parameter hash. @parameters[param.name] = param end alias []= set_parameter def to_hash @parameters.inject({}) do |hash, ary| param = ary[1] # Skip "undef" and nil values. hash[param.name] = param.value if param.value != :undef && !param.value.nil? hash end end # Convert this resource to a RAL resource. def to_ral copy_as_resource.to_ral end # Is the receiver tagged with the given tags? # This match takes into account the tags that a resource will inherit from its container # but have not been set yet. # It does *not* take tags set via resource defaults as these will *never* be set on # the resource itself since all resources always have tags that are automatically # assigned. # def tagged?(*tags) super || ((scope_resource = scope.resource) && scope_resource != self && scope_resource.tagged?(tags)) end private # Add default values from our definition. def add_defaults scope.lookupdefaults(self.type).each do |name, param| unless @parameters.include?(name) self.debug "Adding default for #{name}" @parameters[name] = param.dup end end end def add_scope_tags if scope_resource = scope.resource tag(*scope_resource.tags) end end # Accept a parameter from an override. def override_parameter(param) # This can happen if the override is defining a new parameter, rather # than replacing an existing one. (set_parameter(param) and return) unless current = @parameters[param.name] # The parameter is already set. Fail if they're not allowed to override it. unless param.source.child_of?(current.source) msg = "Parameter '#{param.name}' is already set on #{self}" msg += " by #{current.source}" if current.source.to_s != "" if current.file or current.line fields = [] fields << current.file if current.file fields << current.line.to_s if current.line msg += " at #{fields.join(":")}" end msg += "; cannot redefine" raise Puppet::ParseError.new(msg, param.line, param.file) end # If we've gotten this far, we're allowed to override. # Merge with previous value, if the parameter was generated with the +> # syntax. It's important that we use a copy of the new param instance # here, not the old one, and not the original new one, so that the source # is registered correctly for later overrides but the values aren't # implcitly shared when multiple resources are overrriden at once (see # ticket #3556). if param.add param = param.dup param.value = [current.value, param.value].flatten end set_parameter(param) end # Make sure the resource's parameters are all valid for the type. def validate @parameters.each do |name, param| validate_parameter(name) end rescue => detail self.fail Puppet::ParseError, detail.to_s + " on #{self}", detail end def extract_parameters(params) params.each do |param| # Don't set the same parameter twice self.fail Puppet::ParseError, "Duplicate parameter '#{param.name}' for on #{self}" if @parameters[param.name] set_parameter(param) end end end diff --git a/lib/puppet/parser/resource/param.rb b/lib/puppet/parser/resource/param.rb index 423ae65f1..cb81043ba 100644 --- a/lib/puppet/parser/resource/param.rb +++ b/lib/puppet/parser/resource/param.rb @@ -1,25 +1,25 @@ require 'puppet/parser/yaml_trimmer' # The parameters we stick in Resources. class Puppet::Parser::Resource::Param include Puppet::Util include Puppet::Util::Errors include Puppet::Util::MethodHelper include Puppet::Parser::YamlTrimmer attr_accessor :name, :value, :source, :add, :file, :line def initialize(hash) set_options(hash) - requiredopts(:name, :value) + requiredopts(:name) @name = @name.intern end def line_to_i line ? Integer(line) : nil end def to_s "#{self.name} => #{self.value}" end end diff --git a/lib/puppet/pops/evaluator/evaluator_impl.rb b/lib/puppet/pops/evaluator/evaluator_impl.rb index 02098d674..94e7a2a8b 100644 --- a/lib/puppet/pops/evaluator/evaluator_impl.rb +++ b/lib/puppet/pops/evaluator/evaluator_impl.rb @@ -1,1115 +1,1115 @@ require 'rgen/ecore/ecore' require 'puppet/pops/evaluator/compare_operator' require 'puppet/pops/evaluator/relationship_operator' require 'puppet/pops/evaluator/access_operator' require 'puppet/pops/evaluator/closure' require 'puppet/pops/evaluator/external_syntax_support' # This implementation of {Puppet::Pops::Evaluator} performs evaluation using the puppet 3.x runtime system # in a manner largely compatible with Puppet 3.x, but adds new features and introduces constraints. # # The evaluation uses _polymorphic dispatch_ which works by dispatching to the first found method named after # the class or one of its super-classes. The EvaluatorImpl itself mainly deals with evaluation (it currently # also handles assignment), and it uses a delegation pattern to more specialized handlers of some operators # that in turn use polymorphic dispatch; this to not clutter EvaluatorImpl with too much responsibility). # # Since a pattern is used, only the main entry points are fully documented. The parameters _o_ and _scope_ are # the same in all the polymorphic methods, (the type of the parameter _o_ is reflected in the method's name; # either the actual class, or one of its super classes). The _scope_ parameter is always the scope in which # the evaluation takes place. If nothing else is mentioned, the return is always the result of evaluation. # # See {Puppet::Pops::Visitable} and {Puppet::Pops::Visitor} for more information about # polymorphic calling. # class Puppet::Pops::Evaluator::EvaluatorImpl include Puppet::Pops::Utils # Provides access to the Puppet 3.x runtime (scope, etc.) # This separation has been made to make it easier to later migrate the evaluator to an improved runtime. # include Puppet::Pops::Evaluator::Runtime3Support include Puppet::Pops::Evaluator::ExternalSyntaxSupport # This constant is not defined as Float::INFINITY in Ruby 1.8.7 (but is available in later version # Refactor when support is dropped for Ruby 1.8.7. # INFINITY = 1.0 / 0.0 EMPTY_STRING = ''.freeze COMMA_SEPARATOR = ', '.freeze # Reference to Issues name space makes it easier to refer to issues # (Issues are shared with the validator). # Issues = Puppet::Pops::Issues def initialize @@eval_visitor ||= Puppet::Pops::Visitor.new(self, "eval", 1, 1) @@lvalue_visitor ||= Puppet::Pops::Visitor.new(self, "lvalue", 1, 1) @@assign_visitor ||= Puppet::Pops::Visitor.new(self, "assign", 3, 3) @@string_visitor ||= Puppet::Pops::Visitor.new(self, "string", 1, 1) @@type_calculator ||= Puppet::Pops::Types::TypeCalculator.new() @@type_parser ||= Puppet::Pops::Types::TypeParser.new() @@compare_operator ||= Puppet::Pops::Evaluator::CompareOperator.new() @@relationship_operator ||= Puppet::Pops::Evaluator::RelationshipOperator.new() # Initialize the runtime module Puppet::Pops::Evaluator::Runtime3Support.instance_method(:initialize).bind(self).call() end # @api private def type_calculator @@type_calculator end # Evaluates the given _target_ object in the given scope. # # @overload evaluate(target, scope) # @param target [Object] evaluation target - see methods on the pattern assign_TYPE for actual supported types. # @param scope [Object] the runtime specific scope class where evaluation should take place # @return [Object] the result of the evaluation # # @api public # def evaluate(target, scope) begin @@eval_visitor.visit_this_1(self, target, scope) rescue Puppet::Pops::SemanticError => e # a raised issue may not know the semantic target fail(e.issue, e.semantic || target, e.options, e) rescue StandardError => e if e.is_a? Puppet::ParseError # ParseError's are supposed to be fully configured with location information raise e end fail(Issues::RUNTIME_ERROR, target, {:detail => e.message}, e) end end # Assigns the given _value_ to the given _target_. The additional argument _o_ is the instruction that # produced the target/value tuple and it is used to set the origin of the result. # # @param target [Object] assignment target - see methods on the pattern assign_TYPE for actual supported types. # @param value [Object] the value to assign to `target` # @param o [Puppet::Pops::Model::PopsObject] originating instruction # @param scope [Object] the runtime specific scope where evaluation should take place # # @api private # def assign(target, value, o, scope) @@assign_visitor.visit_this_3(self, target, value, o, scope) end # Computes a value that can be used as the LHS in an assignment. # @param o [Object] the expression to evaluate as a left (assignable) entity # @param scope [Object] the runtime specific scope where evaluation should take place # # @api private # def lvalue(o, scope) @@lvalue_visitor.visit_this_1(self, o, scope) end # Produces a String representation of the given object _o_ as used in interpolation. # @param o [Object] the expression of which a string representation is wanted # @param scope [Object] the runtime specific scope where evaluation should take place # # @api public # def string(o, scope) @@string_visitor.visit_this_1(self, o, scope) end # Evaluate a BlockExpression in a new scope with variables bound to the # given values. # # @param scope [Puppet::Parser::Scope] the parent scope # @param variable_bindings [Hash{String => Object}] the variable names and values to bind (names are keys, bound values are values) # @param block [Puppet::Pops::Model::BlockExpression] the sequence of expressions to evaluate in the new scope # # @api private # def evaluate_block_with_bindings(scope, variable_bindings, block_expr) with_guarded_scope(scope) do # change to create local scope_from - cannot give it file and line - # that is the place of the call, not "here" create_local_scope_from(variable_bindings, scope) evaluate(block_expr, scope) end end protected def lvalue_VariableExpression(o, scope) # evaluate the name evaluate(o.expr, scope) end # Catches all illegal lvalues # def lvalue_Object(o, scope) fail(Issues::ILLEGAL_ASSIGNMENT, o) end # Assign value to named variable. # The '$' sign is never part of the name. # @example In Puppet DSL # $name = value # @param name [String] name of variable without $ # @param value [Object] value to assign to the variable # @param o [Puppet::Pops::Model::PopsObject] originating instruction # @param scope [Object] the runtime specific scope where evaluation should take place # @return [value] # def assign_String(name, value, o, scope) if name =~ /::/ fail(Issues::CROSS_SCOPE_ASSIGNMENT, o.left_expr, {:name => name}) end set_variable(name, value, o, scope) value end def assign_Numeric(n, value, o, scope) fail(Issues::ILLEGAL_NUMERIC_ASSIGNMENT, o.left_expr, {:varname => n.to_s}) end # Catches all illegal assignment (e.g. 1 = 2, {'a'=>1} = 2, etc) # def assign_Object(name, value, o, scope) fail(Issues::ILLEGAL_ASSIGNMENT, o) end def eval_Factory(o, scope) evaluate(o.current, scope) end # Evaluates any object not evaluated to something else to itself. def eval_Object o, scope o end # Allows nil to be used as a Nop, Evaluates to nil def eval_NilClass(o, scope) nil end # Evaluates Nop to nil. def eval_Nop(o, scope) nil end # Captures all LiteralValues not handled elsewhere. # def eval_LiteralValue(o, scope) o.value end # Reserved Words fail to evaluate # def eval_ReservedWord(o, scope) fail(Puppet::Pops::Issues::RESERVED_WORD, o, {:word => o.word}) end def eval_LiteralDefault(o, scope) :default end def eval_LiteralUndef(o, scope) nil end # A QualifiedReference (i.e. a capitalized qualified name such as Foo, or Foo::Bar) evaluates to a PType # def eval_QualifiedReference(o, scope) @@type_parser.interpret(o) end def eval_NotExpression(o, scope) ! is_true?(evaluate(o.expr, scope)) end def eval_UnaryMinusExpression(o, scope) - coerce_numeric(evaluate(o.expr, scope), o, scope) end def eval_UnfoldExpression(o, scope) candidate = evaluate(o.expr, scope) case candidate when Array candidate when Hash candidate.to_a else # turns anything else into an array (so result can be unfolded) [candidate] end end # Abstract evaluation, returns array [left, right] with the evaluated result of left_expr and # right_expr # @return > array with result of evaluating left and right expressions # def eval_BinaryExpression o, scope [ evaluate(o.left_expr, scope), evaluate(o.right_expr, scope) ] end # Evaluates assignment with operators =, +=, -= and # # @example Puppet DSL # $a = 1 # $a += 1 # $a -= 1 # def eval_AssignmentExpression(o, scope) name = lvalue(o.left_expr, scope) value = evaluate(o.right_expr, scope) if o.operator == :'=' assign(name, value, o, scope) else fail(Issues::UNSUPPORTED_OPERATOR, o, {:operator => o.operator}) end value end ARITHMETIC_OPERATORS = [:'+', :'-', :'*', :'/', :'%', :'<<', :'>>'] COLLECTION_OPERATORS = [:'+', :'-', :'<<'] # Handles binary expression where lhs and rhs are array/hash or numeric and operator is +, - , *, % / << >> # def eval_ArithmeticExpression(o, scope) left = evaluate(o.left_expr, scope) right = evaluate(o.right_expr, scope) begin result = calculate(left, right, o.operator, o.left_expr, o.right_expr, scope) rescue ArgumentError => e fail(Issues::RUNTIME_ERROR, o, {:detail => e.message}, e) end result end # Handles binary expression where lhs and rhs are array/hash or numeric and operator is +, - , *, % / << >> # def calculate(left, right, operator, left_o, right_o, scope) unless ARITHMETIC_OPERATORS.include?(operator) fail(Issues::UNSUPPORTED_OPERATOR, left_o.eContainer, {:operator => o.operator}) end if (left.is_a?(Array) || left.is_a?(Hash)) && COLLECTION_OPERATORS.include?(operator) # Handle operation on collections case operator when :'+' concatenate(left, right) when :'-' delete(left, right) when :'<<' unless left.is_a?(Array) fail(Issues::OPERATOR_NOT_APPLICABLE, left_o, {:operator => operator, :left_value => left}) end left + [right] end else # Handle operation on numeric left = coerce_numeric(left, left_o, scope) right = coerce_numeric(right, right_o, scope) begin if operator == :'%' && (left.is_a?(Float) || right.is_a?(Float)) # Deny users the fun of seeing severe rounding errors and confusing results fail(Issues::OPERATOR_NOT_APPLICABLE, left_o, {:operator => operator, :left_value => left}) end result = left.send(operator, right) rescue NoMethodError => e fail(Issues::OPERATOR_NOT_APPLICABLE, left_o, {:operator => operator, :left_value => left}) rescue ZeroDivisionError => e fail(Issues::DIV_BY_ZERO, right_o) end if result == INFINITY || result == -INFINITY fail(Issues::RESULT_IS_INFINITY, left_o, {:operator => operator}) end result end end def eval_EppExpression(o, scope) scope["@epp"] = [] evaluate(o.body, scope) result = scope["@epp"].join result end def eval_RenderStringExpression(o, scope) scope["@epp"] << o.value.dup nil end def eval_RenderExpression(o, scope) scope["@epp"] << string(evaluate(o.expr, scope), scope) nil end # Evaluates Puppet DSL ->, ~>, <-, and <~ def eval_RelationshipExpression(o, scope) # First level evaluation, reduction to basic data types or puppet types, the relationship operator then translates this # to the final set of references (turning strings into references, which can not naturally be done by the main evaluator since # all strings should not be turned into references. # real = eval_BinaryExpression(o, scope) @@relationship_operator.evaluate(real, o, scope) end # Evaluates x[key, key, ...] # def eval_AccessExpression(o, scope) left = evaluate(o.left_expr, scope) keys = o.keys.nil? ? [] : o.keys.collect {|key| evaluate(key, scope) } Puppet::Pops::Evaluator::AccessOperator.new(o).access(left, scope, *keys) end # Evaluates <, <=, >, >=, and == # def eval_ComparisonExpression o, scope left = evaluate(o.left_expr, scope) right = evaluate(o.right_expr, scope) begin # Left is a type if left.is_a?(Puppet::Pops::Types::PAnyType) case o.operator when :'==' @@type_calculator.equals(left,right) when :'!=' !@@type_calculator.equals(left,right) when :'<' # left can be assigned to right, but they are not equal @@type_calculator.assignable?(right, left) && ! @@type_calculator.equals(left,right) when :'<=' # left can be assigned to right @@type_calculator.assignable?(right, left) when :'>' # right can be assigned to left, but they are not equal @@type_calculator.assignable?(left,right) && ! @@type_calculator.equals(left,right) when :'>=' # right can be assigned to left @@type_calculator.assignable?(left, right) else fail(Issues::UNSUPPORTED_OPERATOR, o, {:operator => o.operator}) end else case o.operator when :'==' @@compare_operator.equals(left,right) when :'!=' ! @@compare_operator.equals(left,right) when :'<' @@compare_operator.compare(left,right) < 0 when :'<=' @@compare_operator.compare(left,right) <= 0 when :'>' @@compare_operator.compare(left,right) > 0 when :'>=' @@compare_operator.compare(left,right) >= 0 else fail(Issues::UNSUPPORTED_OPERATOR, o, {:operator => o.operator}) end end rescue ArgumentError => e fail(Issues::COMPARISON_NOT_POSSIBLE, o, { :operator => o.operator, :left_value => left, :right_value => right, :detail => e.message}, e) end end # Evaluates matching expressions with type, string or regexp rhs expression. # If RHS is a type, the =~ matches compatible (instance? of) type. # # @example # x =~ /abc.*/ # @example # x =~ "abc.*/" # @example # y = "abc" # x =~ "${y}.*" # @example # [1,2,3] =~ Array[Integer[1,10]] # # Note that a string is not instance? of Regexp, only Regular expressions are. # The Pattern type should instead be used as it is specified as subtype of String. # # @return [Boolean] if a match was made or not. Also sets $0..$n to matchdata in current scope. # def eval_MatchExpression o, scope left = evaluate(o.left_expr, scope) pattern = evaluate(o.right_expr, scope) # matches RHS types as instance of for all types except a parameterized Regexp[R] if pattern.is_a?(Puppet::Pops::Types::PAnyType) # evaluate as instance? of type check matched = @@type_calculator.instance?(pattern, left) # convert match result to Boolean true, or false return o.operator == :'=~' ? !!matched : !matched end begin pattern = Regexp.new(pattern) unless pattern.is_a?(Regexp) rescue StandardError => e fail(Issues::MATCH_NOT_REGEXP, o.right_expr, {:detail => e.message}, e) end unless left.is_a?(String) fail(Issues::MATCH_NOT_STRING, o.left_expr, {:left_value => left}) end matched = pattern.match(left) # nil, or MatchData set_match_data(matched,scope) # creates ephemeral # convert match result to Boolean true, or false o.operator == :'=~' ? !!matched : !matched end # Evaluates Puppet DSL `in` expression # def eval_InExpression o, scope left = evaluate(o.left_expr, scope) right = evaluate(o.right_expr, scope) @@compare_operator.include?(right, left, scope) end # @example # $a and $b # b is only evaluated if a is true # def eval_AndExpression o, scope is_true?(evaluate(o.left_expr, scope)) ? is_true?(evaluate(o.right_expr, scope)) : false end # @example # a or b # b is only evaluated if a is false # def eval_OrExpression o, scope is_true?(evaluate(o.left_expr, scope)) ? true : is_true?(evaluate(o.right_expr, scope)) end # Evaluates each entry of the literal list and creates a new Array # Supports unfolding of entries # @return [Array] with the evaluated content # def eval_LiteralList o, scope unfold([], o.values, scope) end # Evaluates each entry of the literal hash and creates a new Hash. # @return [Hash] with the evaluated content # def eval_LiteralHash o, scope # optimized o.entries.reduce({}) {|h,entry| h[evaluate(entry.key, scope)] = evaluate(entry.value, scope); h } end # Evaluates all statements and produces the last evaluated value # def eval_BlockExpression o, scope r = nil o.statements.each {|s| r = evaluate(s, scope)} r end # Performs optimized search over case option values, lazily evaluating each # until there is a match. If no match is found, the case expression's default expression # is evaluated (it may be nil or Nop if there is no default, thus producing nil). # If an option matches, the result of evaluating that option is returned. # @return [Object, nil] what a matched option returns, or nil if nothing matched. # def eval_CaseExpression(o, scope) # memo scope level before evaluating test - don't want a match in the case test to leak $n match vars # to expressions after the case expression. # with_guarded_scope(scope) do test = evaluate(o.test, scope) result = nil the_default = nil if o.options.find do |co| # the first case option that matches if co.values.find do |c| case c when Puppet::Pops::Model::LiteralDefault the_default = co.then_expr is_match?(test, evaluate(c, scope), c, scope) when Puppet::Pops::Model::UnfoldExpression # not ideal for error reporting, since it is not known which unfolded result # that caused an error - the entire unfold expression is blamed (i.e. the var c, passed to is_match?) evaluate(c, scope).any? {|v| is_match?(test, v, c, scope) } else is_match?(test, evaluate(c, scope), c, scope) end end result = evaluate(co.then_expr, scope) true # the option was picked end end result # an option was picked, and produced a result else evaluate(the_default, scope) # evaluate the default (should be a nop/nil) if there is no default). end end end # Evaluates a CollectExpression by transforming it into a 3x AST::Collection and then evaluating that. # This is done because of the complex API between compiler, indirector, backends, and difference between # collecting virtual resources and exported resources. # def eval_CollectExpression o, scope # The Collect Expression and its contained query expressions are implemented in such a way in # 3x that it is almost impossible to do anything about them (the AST objects are lazily evaluated, # and the built structure consists of both higher order functions and arrays with query expressions # that are either used as a predicate filter, or given to an indirection terminus (such as the Puppet DB # resource terminus). Unfortunately, the 3x implementation has many inconsistencies that the implementation # below carries forward. # collect_3x = Puppet::Pops::Model::AstTransformer.new().transform(o) collected = collect_3x.evaluate(scope) # the 3x returns an instance of Parser::Collector (but it is only registered with the compiler at this # point and does not contain any valuable information (like the result) # Dilemma: If this object is returned, it is a first class value in the Puppet Language and we # need to be able to perform operations on it. We can forbid it from leaking by making CollectExpression # a non R-value. This makes it possible for the evaluator logic to make use of the Collector. collected end def eval_ParenthesizedExpression(o, scope) evaluate(o.expr, scope) end # This evaluates classes, nodes and resource type definitions to nil, since 3x: # instantiates them, and evaluates their parameters and body. This is achieved by # providing bridge AST classes in Puppet::Parser::AST::PopsBridge that bridges a # Pops Program and a Pops Expression. # # Since all Definitions are handled "out of band", they are treated as a no-op when # evaluated. # def eval_Definition(o, scope) nil end def eval_Program(o, scope) evaluate(o.body, scope) end # Produces Array[PAnyType], an array of resource references # def eval_ResourceExpression(o, scope) exported = o.exported virtual = o.virtual # Get the type name type_name = if (tmp_name = o.type_name).is_a?(Puppet::Pops::Model::QualifiedName) tmp_name.value # already validated as a name else type_name_acceptable = case o.type_name when Puppet::Pops::Model::QualifiedReference true when Puppet::Pops::Model::AccessExpression o.type_name.left_expr.is_a?(Puppet::Pops::Model::QualifiedReference) end evaluated_name = evaluate(tmp_name, scope) unless type_name_acceptable actual = type_calculator.generalize!(type_calculator.infer(evaluated_name)).to_s fail(Puppet::Pops::Issues::ILLEGAL_RESOURCE_TYPE, o.type_name, {:actual => actual}) end # must be a CatalogEntry subtype case evaluated_name when Puppet::Pops::Types::PHostClassType unless evaluated_name.class_name.nil? fail(Puppet::Pops::Issues::ILLEGAL_RESOURCE_TYPE, o.type_name, {:actual=> evaluated_name.to_s}) end 'class' when Puppet::Pops::Types::PResourceType unless evaluated_name.title().nil? fail(Puppet::Pops::Issues::ILLEGAL_RESOURCE_TYPE, o.type_name, {:actual=> evaluated_name.to_s}) end evaluated_name.type_name # assume validated else actual = type_calculator.generalize!(type_calculator.infer(evaluated_name)).to_s fail(Puppet::Pops::Issues::ILLEGAL_RESOURCE_TYPE, o.type_name, {:actual=>actual}) end end # This is a runtime check - the model is valid, but will have runtime issues when evaluated # and storeconfigs is not set. if(o.exported) optionally_fail(Puppet::Pops::Issues::RT_NO_STORECONFIGS_EXPORT, o); end titles_to_body = {} body_to_titles = {} body_to_params = {} # titles are evaluated before attribute operations o.bodies.map do | body | titles = evaluate(body.title, scope) # Title may not be nil # Titles may be given as an array, it is ok if it is empty, but not if it contains nil entries # Titles may not be an empty String # Titles must be unique in the same resource expression # There may be a :default entry, its entries apply with lower precedence # if titles.nil? fail(Puppet::Pops::Issues::MISSING_TITLE, body.title) end titles = [titles].flatten # Check types of evaluated titles and duplicate entries titles.each_with_index do |title, index| if title.nil? fail(Puppet::Pops::Issues::MISSING_TITLE_AT, body.title, {:index => index}) elsif !title.is_a?(String) && title != :default actual = type_calculator.generalize!(type_calculator.infer(title)).to_s fail(Puppet::Pops::Issues::ILLEGAL_TITLE_TYPE_AT, body.title, {:index => index, :actual => actual}) elsif title == EMPTY_STRING fail(Puppet::Pops::Issues::EMPTY_STRING_TITLE_AT, body.title, {:index => index}) elsif titles_to_body[title] fail(Puppet::Pops::Issues::DUPLICATE_TITLE, o, {:title => title}) end titles_to_body[title] = body end # Do not create a real instance from the :default case titles.delete(:default) body_to_titles[body] = titles # Store evaluated parameters in a hash associated with the body, but do not yet create resource # since the entry containing :defaults may appear later body_to_params[body] = body.operations.reduce({}) do |param_memo, op| params = evaluate(op, scope) params = [params] unless params.is_a?(Array) params.each do |p| if param_memo.include? p.name fail(Puppet::Pops::Issues::DUPLICATE_ATTRIBUTE, o, {:attribute => p.name}) end - param_memo[p.name] = p + param_memo[p.name] = p end param_memo end end # Titles and Operations have now been evaluated and resources can be created # Each production is a PResource, and an array of all is produced as the result of # evaluating the ResourceExpression. # defaults_hash = body_to_params[titles_to_body[:default]] || {} o.bodies.map do | body | titles = body_to_titles[body] params = defaults_hash.merge(body_to_params[body] || {}) create_resources(o, scope, virtual, exported, type_name, titles, params.values) end.flatten.compact end def eval_ResourceOverrideExpression(o, scope) evaluated_resources = evaluate(o.resources, scope) evaluated_parameters = o.operations.map { |op| evaluate(op, scope) } create_resource_overrides(o, scope, [evaluated_resources].flatten, evaluated_parameters) evaluated_resources end # Produces 3x parameter def eval_AttributeOperation(o, scope) create_resource_parameter(o, scope, o.attribute_name, evaluate(o.value_expr, scope), o.operator) end def eval_AttributesOperation(o, scope) hashed_params = evaluate(o.expr, scope) unless hashed_params.is_a?(Hash) actual = type_calculator.generalize!(type_calculator.infer(hashed_params)).to_s fail(Puppet::Pops::Issues::TYPE_MISMATCH, o.expr, {:expected => 'Hash', :actual => actual}) end hashed_params.map { |k,v| create_resource_parameter(o, scope, k, v, :'=>') } end # Sets default parameter values for a type, produces the type # def eval_ResourceDefaultsExpression(o, scope) type = evaluate(o.type_ref, scope) type_name = if type.is_a?(Puppet::Pops::Types::PResourceType) && !type.type_name.nil? && type.title.nil? type.type_name # assume it is a valid name else actual = type_calculator.generalize!(type_calculator.infer(type)) fail(Issues::ILLEGAL_RESOURCE_TYPE, o.type_ref, {:actual => actual}) end evaluated_parameters = o.operations.map {|op| evaluate(op, scope) } create_resource_defaults(o, scope, type_name, evaluated_parameters) # Produce the type type end # Evaluates function call by name. # def eval_CallNamedFunctionExpression(o, scope) # The functor expression is not evaluated, it is not possible to select the function to call # via an expression like $a() case o.functor_expr when Puppet::Pops::Model::QualifiedName # ok when Puppet::Pops::Model::RenderStringExpression # helpful to point out this easy to make Epp error fail(Issues::ILLEGAL_EPP_PARAMETERS, o) else fail(Issues::ILLEGAL_EXPRESSION, o.functor_expr, {:feature=>'function name', :container => o}) end name = o.functor_expr.value evaluated_arguments = unfold([], o.arguments, scope) # wrap lambda in a callable block if it is present evaluated_arguments << Puppet::Pops::Evaluator::Closure.new(self, o.lambda, scope) if o.lambda call_function(name, evaluated_arguments, o, scope) end # Evaluation of CallMethodExpression handles a NamedAccessExpression functor (receiver.function_name) # def eval_CallMethodExpression(o, scope) unless o.functor_expr.is_a? Puppet::Pops::Model::NamedAccessExpression fail(Issues::ILLEGAL_EXPRESSION, o.functor_expr, {:feature=>'function accessor', :container => o}) end receiver = evaluate(o.functor_expr.left_expr, scope) name = o.functor_expr.right_expr unless name.is_a? Puppet::Pops::Model::QualifiedName fail(Issues::ILLEGAL_EXPRESSION, o.functor_expr, {:feature=>'function name', :container => o}) end name = name.value # the string function name evaluated_arguments = unfold([receiver], o.arguments || [], scope) # wrap lambda in a callable block if it is present evaluated_arguments << Puppet::Pops::Evaluator::Closure.new(self, o.lambda, scope) if o.lambda call_function(name, evaluated_arguments, o, scope) end # @example # $x ? { 10 => true, 20 => false, default => 0 } # def eval_SelectorExpression o, scope # memo scope level before evaluating test - don't want a match in the case test to leak $n match vars # to expressions after the selector expression. # with_guarded_scope(scope) do test = evaluate(o.left_expr, scope) the_default = nil selected = o.selectors.find do |s| me = s.matching_expr case me when Puppet::Pops::Model::LiteralDefault the_default = s.value_expr false when Puppet::Pops::Model::UnfoldExpression # not ideal for error reporting, since it is not known which unfolded result # that caused an error - the entire unfold expression is blamed (i.e. the var c, passed to is_match?) evaluate(me, scope).any? {|v| is_match?(test, v, me, scope) } else is_match?(test, evaluate(me, scope), me, scope) end end if selected evaluate(selected.value_expr, scope) elsif the_default evaluate(the_default, scope) else fail(Issues::UNMATCHED_SELECTOR, o.left_expr, :param_value => test) end end end # SubLocatable is simply an expression that holds location information def eval_SubLocatedExpression o, scope evaluate(o.expr, scope) end # Evaluates Puppet DSL Heredoc def eval_HeredocExpression o, scope result = evaluate(o.text_expr, scope) assert_external_syntax(scope, result, o.syntax, o.text_expr) result end # Evaluates Puppet DSL `if` def eval_IfExpression o, scope with_guarded_scope(scope) do if is_true?(evaluate(o.test, scope)) evaluate(o.then_expr, scope) else evaluate(o.else_expr, scope) end end end # Evaluates Puppet DSL `unless` def eval_UnlessExpression o, scope with_guarded_scope(scope) do unless is_true?(evaluate(o.test, scope)) evaluate(o.then_expr, scope) else evaluate(o.else_expr, scope) end end end # Evaluates a variable (getting its value) # The evaluator is lenient; any expression producing a String is used as a name # of a variable. # def eval_VariableExpression o, scope # Evaluator is not too fussy about what constitutes a name as long as the result # is a String and a valid variable name # name = evaluate(o.expr, scope) # Should be caught by validation, but make this explicit here as well, or mysterious evaluation issues # may occur for some evaluation use cases. case name when String when Numeric else fail(Issues::ILLEGAL_VARIABLE_EXPRESSION, o.expr) end get_variable_value(name, o, scope) end # Evaluates double quoted strings that may contain interpolation # def eval_ConcatenatedString o, scope o.segments.collect {|expr| string(evaluate(expr, scope), scope)}.join end # If the wrapped expression is a QualifiedName, it is taken as the name of a variable in scope. # Note that this is different from the 3.x implementation, where an initial qualified name # is accepted. (e.g. `"---${var + 1}---"` is legal. This implementation requires such concrete # syntax to be expressed in a model as `(TextExpression (+ (Variable var) 1)` - i.e. moving the decision to # the parser. # # Semantics; the result of an expression is turned into a string, nil is silently transformed to empty # string. # @return [String] the interpolated result # def eval_TextExpression o, scope if o.expr.is_a?(Puppet::Pops::Model::QualifiedName) string(get_variable_value(o.expr.value, o, scope), scope) else string(evaluate(o.expr, scope), scope) end end def string_Object(o, scope) o.to_s end def string_Symbol(o, scope) if :undef == o # optimized comparison 1.44 vs 1.95 EMPTY_STRING else o.to_s end end def string_Array(o, scope) "[#{o.map {|e| string(e, scope)}.join(COMMA_SEPARATOR)}]" end def string_Hash(o, scope) "{#{o.map {|k,v| "#{string(k, scope)} => #{string(v, scope)}"}.join(COMMA_SEPARATOR)}}" end def string_Regexp(o, scope) "/#{o.source}/" end def string_PAnyType(o, scope) @@type_calculator.string(o) end # Produces concatenation / merge of x and y. # # When x is an Array, y of type produces: # # * Array => concatenation `[1,2], [3,4] => [1,2,3,4]` # * Hash => concatenation of hash as array `[key, value, key, value, ...]` # * any other => concatenation of single value # # When x is a Hash, y of type produces: # # * Array => merge of array interpreted as `[key, value, key, value,...]` # * Hash => a merge, where entries in `y` overrides # * any other => error # # When x is something else, wrap it in an array first. # # When x is nil, an empty array is used instead. # # @note to concatenate an Array, nest the array - i.e. `[1,2], [[2,3]]` # # @overload concatenate(obj_x, obj_y) # @param obj_x [Object] object to wrap in an array and concatenate to; see other overloaded methods for return type # @param ary_y [Object] array to concatenate at end of `ary_x` # @return [Object] wraps obj_x in array before using other overloaded option based on type of obj_y # @overload concatenate(ary_x, ary_y) # @param ary_x [Array] array to concatenate to # @param ary_y [Array] array to concatenate at end of `ary_x` # @return [Array] new array with `ary_x` + `ary_y` # @overload concatenate(ary_x, hsh_y) # @param ary_x [Array] array to concatenate to # @param hsh_y [Hash] converted to array form, and concatenated to array # @return [Array] new array with `ary_x` + `hsh_y` converted to array # @overload concatenate (ary_x, obj_y) # @param ary_x [Array] array to concatenate to # @param obj_y [Object] non array or hash object to add to array # @return [Array] new array with `ary_x` + `obj_y` added as last entry # @overload concatenate(hsh_x, ary_y) # @param hsh_x [Hash] the hash to merge with # @param ary_y [Array] array interpreted as even numbered sequence of key, value merged with `hsh_x` # @return [Hash] new hash with `hsh_x` merged with `ary_y` interpreted as hash in array form # @overload concatenate(hsh_x, hsh_y) # @param hsh_x [Hash] the hash to merge to # @param hsh_y [Hash] hash merged with `hsh_x` # @return [Hash] new hash with `hsh_x` merged with `hsh_y` # @raise [ArgumentError] when `xxx_x` is neither an Array nor a Hash # @raise [ArgumentError] when `xxx_x` is a Hash, and `xxx_y` is neither Array nor Hash. # def concatenate(x, y) x = [x] unless x.is_a?(Array) || x.is_a?(Hash) case x when Array y = case y when Array then y when Hash then y.to_a else [y] end x + y # new array with concatenation when Hash y = case y when Hash then y when Array # Hash[[a, 1, b, 2]] => {} # Hash[a,1,b,2] => {a => 1, b => 2} # Hash[[a,1], [b,2]] => {[a,1] => [b,2]} # Hash[[[a,1], [b,2]]] => {a => 1, b => 2} # Use type calcultor to determine if array is Array[Array[?]], and if so use second form # of call t = @@type_calculator.infer(y) if t.element_type.is_a? Puppet::Pops::Types::PArrayType Hash[y] else Hash[*y] end else raise ArgumentError.new("Can only append Array or Hash to a Hash") end x.merge y # new hash with overwrite else raise ArgumentError.new("Can only append to an Array or a Hash.") end end # Produces the result x \ y (set difference) # When `x` is an Array, `y` is transformed to an array and then all matching elements removed from x. # When `x` is a Hash, all contained keys are removed from x as listed in `y` if it is an Array, or all its keys if it is a Hash. # The difference is returned. The given `x` and `y` are not modified by this operation. # @raise [ArgumentError] when `x` is neither an Array nor a Hash # def delete(x, y) result = x.dup case x when Array y = case y when Array then y when Hash then y.to_a else [y] end y.each {|e| result.delete(e) } when Hash y = case y when Array then y when Hash then y.keys else [y] end y.each {|e| result.delete(e) } else raise ArgumentError.new("Can only delete from an Array or Hash.") end result end # Implementation of case option matching. # # This is the type of matching performed in a case option, using == for every type # of value except regular expression where a match is performed. # def is_match? left, right, o, scope if right.is_a?(Regexp) return false unless left.is_a? String matched = right.match(left) set_match_data(matched, scope) # creates or clears ephemeral !!matched # convert to boolean elsif right.is_a?(Puppet::Pops::Types::PAnyType) # right is a type and left is not - check if left is an instance of the given type # (The reverse is not terribly meaningful - computing which of the case options that first produces # an instance of a given type). # @@type_calculator.instance?(right, left) else # Handle equality the same way as the language '==' operator (case insensitive etc.) @@compare_operator.equals(left,right) end end def with_guarded_scope(scope) scope_memo = get_scope_nesting_level(scope) begin yield ensure set_scope_nesting_level(scope, scope_memo) end end # Maps the expression in the given array to their product except for UnfoldExpressions which are first unfolded. # The result is added to the given result Array. # @param result [Array] Where to add the result (may contain information to add to) # @param array [Array[Puppet::Pops::Model::Expression] the expressions to map # @param scope [Puppet::Parser::Scope] the scope to evaluate in # @return [Array] the given result array with content added from the operation # def unfold(result, array, scope) array.each do |x| if x.is_a?(Puppet::Pops::Model::UnfoldExpression) result.concat(evaluate(x, scope)) else result << evaluate(x, scope) end end result end private :unfold end diff --git a/lib/puppet/pops/evaluator/runtime3_support.rb b/lib/puppet/pops/evaluator/runtime3_support.rb index ca15eb401..26086ed6d 100644 --- a/lib/puppet/pops/evaluator/runtime3_support.rb +++ b/lib/puppet/pops/evaluator/runtime3_support.rb @@ -1,602 +1,601 @@ # A module with bindings between the new evaluator and the 3x runtime. # The intention is to separate all calls into scope, compiler, resource, etc. in this module # to make it easier to later refactor the evaluator for better implementations of the 3x classes. # # @api private module Puppet::Pops::Evaluator::Runtime3Support NAME_SPACE_SEPARATOR = '::'.freeze # Fails the evaluation of _semantic_ with a given issue. # # @param issue [Puppet::Pops::Issue] the issue to report # @param semantic [Puppet::Pops::ModelPopsObject] the object for which evaluation failed in some way. Used to determine origin. # @param options [Hash] hash of optional named data elements for the given issue # @return [!] this method does not return # @raise [Puppet::ParseError] an evaluation error initialized from the arguments (TODO: Change to EvaluationError?) # def fail(issue, semantic, options={}, except=nil) optionally_fail(issue, semantic, options, except) # an error should have been raised since fail always fails raise ArgumentError, "Internal Error: Configuration of runtime error handling wrong: should have raised exception" end # Optionally (based on severity) Fails the evaluation of _semantic_ with a given issue # If the given issue is configured to be of severity < :error it is only reported, and the function returns. # # @param issue [Puppet::Pops::Issue] the issue to report # @param semantic [Puppet::Pops::ModelPopsObject] the object for which evaluation failed in some way. Used to determine origin. # @param options [Hash] hash of optional named data elements for the given issue # @return [!] this method does not return # @raise [Puppet::ParseError] an evaluation error initialized from the arguments (TODO: Change to EvaluationError?) # def optionally_fail(issue, semantic, options={}, except=nil) if except.nil? # Want a stacktrace, and it must be passed as an exception begin raise EvaluationError.new() rescue EvaluationError => e except = e end end diagnostic_producer.accept(issue, semantic, options, except) end # Binds the given variable name to the given value in the given scope. # The reference object `o` is intended to be used for origin information - the 3x scope implementation # only makes use of location when there is an error. This is now handled by other mechanisms; first a check # is made if a variable exists and an error is raised if attempting to change an immutable value. Errors # in name, numeric variable assignment etc. have also been validated prior to this call. In the event the # scope.setvar still raises an error, the general exception handling for evaluation of the assignment # expression knows about its location. Because of this, there is no need to extract the location for each # setting (extraction is somewhat expensive since 3x requires line instead of offset). # def set_variable(name, value, o, scope) # Scope also checks this but requires that location information are passed as options. # Those are expensive to calculate and a test is instead made here to enable failing with better information. # The error is not specific enough to allow catching it - need to check the actual message text. # TODO: Improve the messy implementation in Scope. # if scope.bound?(name) if Puppet::Parser::Scope::RESERVED_VARIABLE_NAMES.include?(name) fail(Puppet::Pops::Issues::ILLEGAL_RESERVED_ASSIGNMENT, o, {:name => name} ) else fail(Puppet::Pops::Issues::ILLEGAL_REASSIGNMENT, o, {:name => name} ) end end scope.setvar(name, value) end # Returns the value of the variable (nil is returned if variable has no value, or if variable does not exist) # def get_variable_value(name, o, scope) # Puppet 3x stores all variables as strings (then converts them back to numeric with a regexp... to see if it is a match variable) # Not ideal, scope should support numeric lookup directly instead. # TODO: consider fixing scope catch(:undefined_variable) { x = scope.lookupvar(name.to_s) # Must convert :undef back to nil - this can happen when an undefined variable is used in a # parameter's default value expression - there nil must be :undef to work with the rest of 3x. # Now that the value comes back to 4x it is changed to nil. return (x == :undef) ? nil : x } # It is always ok to reference numeric variables even if they are not assigned. They are always undef # if not set by a match expression. # unless name =~ Puppet::Pops::Patterns::NUMERIC_VAR_NAME fail(Puppet::Pops::Issues::UNKNOWN_VARIABLE, o, {:name => name}) end end # Returns true if the variable of the given name is set in the given most nested scope. True is returned even if # variable is bound to nil. # def variable_bound?(name, scope) scope.bound?(name.to_s) end # Returns true if the variable is bound to a value or nil, in the scope or it's parent scopes. # def variable_exists?(name, scope) scope.exist?(name.to_s) end def set_match_data(match_data, scope) # See set_variable for rationale for not passing file and line to ephemeral_from. # NOTE: The 3x scope adds one ephemeral(match) to its internal stack per match that succeeds ! It never # clears anything. Thus a context that performs many matches will get very deep (there simply is no way to # clear the match variables without rolling back the ephemeral stack.) # This implementation does not attempt to fix this, it behaves the same bad way. unless match_data.nil? scope.ephemeral_from(match_data) end end # Creates a local scope with vairalbes set from a hash of variable name to value # def create_local_scope_from(hash, scope) # two dummy values are needed since the scope tries to give an error message (can not happen in this # case - it is just wrong, the error should be reported by the caller who knows in more detail where it # is in the source. # raise ArgumentError, "Internal error - attempt to create a local scope without a hash" unless hash.is_a?(Hash) scope.ephemeral_from(hash) end # Creates a nested match scope def create_match_scope_from(scope) # Create a transparent match scope (for future matches) scope.new_match_scope(nil) end def get_scope_nesting_level(scope) scope.ephemeral_level end def set_scope_nesting_level(scope, level) # Yup, 3x uses this method to reset the level, it also supports passing :all to destroy all # ephemeral/local scopes - which is a sure way to create havoc. # scope.unset_ephemeral_var(level) end # Adds a relationship between the given `source` and `target` of the given `relationship_type` # @param source [Puppet:Pops::Types::PCatalogEntryType] the source end of the relationship (from) # @param target [Puppet:Pops::Types::PCatalogEntryType] the target end of the relationship (to) # @param relationship_type [:relationship, :subscription] the type of the relationship # def add_relationship(source, target, relationship_type, scope) # The 3x way is to record a Puppet::Parser::Relationship that is evaluated at the end of the compilation. # This means it is not possible to detect any duplicates at this point (and signal where an attempt is made to # add a duplicate. There is also no location information to signal the original place in the logic. The user will have # to go fish. # The 3.x implementation is based on Strings :-o, so the source and target must be transformed. The resolution is # done by Catalog#resource(type, title). To do that, it creates a Puppet::Resource since it is responsible for # translating the name/type/title and create index-keys used by the catalog. The Puppet::Resource has bizarre parsing of # the type and title (scan for [] that is interpreted as type/title (but it gets it wrong). # Moreover if the type is "" or "component", the type is Class, and if the type is :main, it is :main, all other cases # undergo capitalization of name-segments (foo::bar becomes Foo::Bar). (This was earlier done in the reverse by the parser). # Further, the title undergoes the same munging !!! # # That bug infested nest of messy logic needs serious Exorcism! # # Unfortunately it is not easy to simply call more intelligent methods at a lower level as the compiler evaluates the recorded # Relationship object at a much later point, and it is responsible for invoking all the messy logic. # # TODO: Revisit the below logic when there is a sane implementation of the catalog, compiler and resource. For now # concentrate on transforming the type references to what is expected by the wacky logic. # # HOWEVER, the Compiler only records the Relationships, and the only method it calls is @relationships.each{|x| x.evaluate(catalog) } # Which means a smarter Relationship class could do this right. Instead of obtaining the resource from the catalog using # the borked resource(type, title) which creates a resource for the purpose of looking it up, it needs to instead # scan the catalog's resources # # GAAAH, it is even worse! # It starts in the parser, which parses "File['foo']" into an AST::ResourceReference with type = File, and title = foo # This AST is evaluated by looking up the type/title in the scope - causing it to be loaded if it exists, and if not, the given # type name/title is used. It does not search for resource instances, only classes and types. It returns symbolic information # [type, [title, title]]. From this, instances of Puppet::Resource are created and returned. These only have type/title information # filled out. One or an array of resources are returned. # This set of evaluated (empty reference) Resource instances are then passed to the relationship operator. It creates a # Puppet::Parser::Relationship giving it a source and a target that are (empty reference) Resource instances. These are then remembered # until the relationship is evaluated by the compiler (at the end). When evaluation takes place, the (empty reference) Resource instances # are converted to String (!?! WTF) on the simple format "#{type}[#{title}]", and the catalog is told to find a resource, by giving # it this string. If it cannot find the resource it fails, else the before/notify parameter is appended with the target. # The search for the resource begin with (you guessed it) again creating an (empty reference) resource from type and title (WTF?!?!). # The catalog now uses the reference resource to compute a key [r.type, r.title.to_s] and also gets a uniqueness key from the # resource (This is only a reference type created from title and type). If it cannot find it with the first key, it uses the # uniqueness key to lookup. # # This is probably done to allow a resource type to munge/translate the title in some way (but it is quite unclear from the long # and convoluted path of evaluation. # In order to do this in a way that is similar to 3.x two resources are created to be used as keys. # # And if that is not enough, a source/target may be a Collector (a baked query that will be evaluated by the # compiler - it is simply passed through here for processing by the compiler at the right time). # if source.is_a?(Puppet::Parser::Collector) # use verbatim - behavior defined by 3x source_resource = source else # transform into the wonderful String representation in 3x type, title = catalog_type_to_split_type_title(source) source_resource = Puppet::Resource.new(type, title) end if target.is_a?(Puppet::Parser::Collector) # use verbatim - behavior defined by 3x target_resource = target else # transform into the wonderful String representation in 3x type, title = catalog_type_to_split_type_title(target) target_resource = Puppet::Resource.new(type, title) end # Add the relationship to the compiler for later evaluation. scope.compiler.add_relationship(Puppet::Parser::Relationship.new(source_resource, target_resource, relationship_type)) end # Coerce value `v` to numeric or fails. # The given value `v` is coerced to Numeric, and if that fails the operation # calls {#fail}. # @param v [Object] the value to convert # @param o [Object] originating instruction # @param scope [Object] the (runtime specific) scope where evaluation of o takes place # @return [Numeric] value `v` converted to Numeric. # def coerce_numeric(v, o, scope) unless n = Puppet::Pops::Utils.to_n(v) fail(Puppet::Pops::Issues::NOT_NUMERIC, o, {:value => v}) end n end def call_function(name, args, o, scope) Puppet::Util::Profiler.profile("Called #{name}", [:functions, name]) do # Call via 4x API if the function exists there loaders = scope.compiler.loaders # find the loader that loaded the code, or use the private_environment_loader (sees env + all modules) adapter = Puppet::Pops::Utils.find_adapter(o, Puppet::Pops::Adapters::LoaderAdapter) loader = adapter.nil? ? loaders.private_environment_loader : adapter.loader if loader && func = loader.load(:function, name) return func.call(scope, *args) end # Call via 3x API if function exists there fail(Puppet::Pops::Issues::UNKNOWN_FUNCTION, o, {:name => name}) unless Puppet::Parser::Functions.function(name) # Arguments must be mapped since functions are unaware of the new and magical creatures in 4x. # NOTE: Passing an empty string last converts nil/:undef to empty string mapped_args = args.map {|a| convert(a, scope, '') } result = scope.send("function_#{name}", mapped_args) # Prevent non r-value functions from leaking their result (they are not written to care about this) Puppet::Parser::Functions.rvalue?(name) ? result : nil end end # The o is used for source reference def create_resource_parameter(o, scope, name, value, operator) file, line = extract_file_line(o) Puppet::Parser::Resource::Param.new( :name => name, - # Here we must convert nil values to :undef for the 3x logic to work - :value => convert(value, scope, :undef), # converted to 3x since 4x supports additional objects / types + :value => convert(value, scope, nil), # converted to 3x since 4x supports additional objects / types :source => scope.source, :line => line, :file => file, :add => operator == :'+>' ) end CLASS_STRING = 'class'.freeze def create_resources(o, scope, virtual, exported, type_name, resource_titles, evaluated_parameters) # TODO: Unknown resource causes creation of Resource to fail with ArgumentError, should give # a proper Issue. Now the result is "Error while evaluating a Resource Statement" with the message # from the raised exception. (It may be good enough). # resolve in scope. fully_qualified_type, resource_titles = scope.resolve_type_and_titles(type_name, resource_titles) # Not 100% accurate as this is the resource expression location and each title is processed separately # The titles are however the result of evaluation and they have no location at this point (an array # of positions for the source expressions are required for this to work). # TODO: Revisit and possible improve the accuracy. # file, line = extract_file_line(o) # Build a resource for each title resource_titles.map do |resource_title| resource = Puppet::Parser::Resource.new( fully_qualified_type, resource_title, :parameters => evaluated_parameters, :file => file, :line => line, :exported => exported, :virtual => virtual, # WTF is this? Which source is this? The file? The name of the context ? :source => scope.source, :scope => scope, :strict => true ) if resource.resource_type.is_a? Puppet::Resource::Type resource.resource_type.instantiate_resource(scope, resource) end scope.compiler.add_resource(scope, resource) scope.compiler.evaluate_classes([resource_title], scope, false, true) if fully_qualified_type == CLASS_STRING # Turn the resource into a PType (a reference to a resource type) # weed out nil's resource_to_ptype(resource) end end # Defines default parameters for a type with the given name. # def create_resource_defaults(o, scope, type_name, evaluated_parameters) # Note that name must be capitalized in this 3x call # The 3x impl creates a Resource instance with a bogus title and then asks the created resource # for the type of the name. # Note, locations are available per parameter. # scope.define_settings(capitalize_qualified_name(type_name), evaluated_parameters) end # Capitalizes each segment of a qualified name # def capitalize_qualified_name(name) name.split(/::/).map(&:capitalize).join(NAME_SPACE_SEPARATOR) end # Creates resource overrides for all resource type objects in evaluated_resources. The same set of # evaluated parameters are applied to all. # def create_resource_overrides(o, scope, evaluated_resources, evaluated_parameters) # Not 100% accurate as this is the resource expression location and each title is processed separately # The titles are however the result of evaluation and they have no location at this point (an array # of positions for the source expressions are required for this to work. # TODO: Revisit and possible improve the accuracy. # file, line = extract_file_line(o) evaluated_resources.each do |r| unless r.is_a?(Puppet::Pops::Types::PResourceType) && r.type_name != 'class' fail(Puppet::Pops::Issues::ILLEGAL_OVERRIDEN_TYPE, o, {:actual => r} ) end resource = Puppet::Parser::Resource.new( r.type_name, r.title, :parameters => evaluated_parameters, :file => file, :line => line, # WTF is this? Which source is this? The file? The name of the context ? :source => scope.source, :scope => scope ) scope.compiler.add_override(resource) end end # Finds a resource given a type and a title. # def find_resource(scope, type_name, title) scope.compiler.findresource(type_name, title) end # Returns the value of a resource's parameter by first looking up the parameter in the resource # and then in the defaults for the resource. Since the resource exists (it must in order to look up its # parameters, any overrides have already been applied). Defaults are not applied to a resource until it # has been finished (which typically has not taken place when this is evaluated; hence the dual lookup). # def get_resource_parameter_value(scope, resource, parameter_name) # This gets the parameter value, or nil (for both valid parameters and parameters that do not exist). val = resource[parameter_name] # Sometimes the resource is a Puppet::Parser::Resource and sometimes it is # a Puppet::Resource. The Puppet::Resource case occurs when puppet language # is evaluated against an already completed catalog (where all instances of # Puppet::Parser::Resource are converted to Puppet::Resource instances). # Evaluating against an already completed catalog is really only found in # the language specification tests, where the puppet language is used to # test itself. if resource.is_a?(Puppet::Parser::Resource) # The defaults must be looked up in the scope where the resource was created (not in the given # scope where the lookup takes place. resource_scope = resource.scope if val.nil? && resource_scope && defaults = resource_scope.lookupdefaults(resource.type) # NOTE: 3x resource keeps defaults as hash using symbol for name as key to Parameter which (again) holds # name and value. # NOTE: meta parameters that are unset ends up here, and there are no defaults for those encoded # in the defaults, they may receive hardcoded defaults later (e.g. 'tag'). param = defaults[parameter_name.to_sym] # Some parameters (meta parameters like 'tag') does not return a param from which the value can be obtained # at all times. Instead, they return a nil param until a value has been set. val = param.nil? ? nil : param.value end end val end # Returns true, if the given name is the name of a resource parameter. # def is_parameter_of_resource?(scope, resource, name) return false unless name.is_a?(String) resource.valid_parameter?(name) end def resource_to_ptype(resource) nil if resource.nil? # inference returns the meta type since the 3x Resource is an alternate way to describe a type type_calculator.infer(resource).type end # This is the same type of "truth" as used in the current Puppet DSL. # def is_true? o # Is the value true? This allows us to control the definition of truth # in one place. case o # Support :undef since it may come from a 3x structure when :undef false else !!o end end # Utility method for TrueClass || FalseClass # @param x [Object] the object to test if it is instance of TrueClass or FalseClass def is_boolean? x x.is_a?(TrueClass) || x.is_a?(FalseClass) end def initialize @@convert_visitor ||= Puppet::Pops::Visitor.new(self, "convert", 2, 2) @@convert2_visitor ||= Puppet::Pops::Visitor.new(self, "convert2", 2, 2) end # Converts 4x supported values to 3x values. This is required because # resources and other objects do not know about the new type system, and does not support # regular expressions. Unfortunately this has to be done for array and hash as well. # A complication is that catalog types needs to be resolved against the scope. # def convert(o, scope, undef_value) @@convert_visitor.visit_this_2(self, o, scope, undef_value) end # Converts nested 4x supported values to 3x values. This is required because # resources and other objects do not know about the new type system, and does not support # regular expressions. Unfortunately this has to be done for array and hash as well. # A complication is that catalog types needs to be resolved against the scope. # def convert2(o, scope, undef_value) @@convert2_visitor.visit_this_2(self, o, scope, undef_value) end def convert_NilClass(o, scope, undef_value) undef_value end def convert2_NilClass(o, scope, undef_value) :undef end def convert_String(o, scope, undef_value) # although wasteful, needed because user code may mutate these strings in Resources o.frozen? ? o.dup : o end alias convert2_String :convert_String def convert_Object(o, scope, undef_value) o end alias :convert2_Object :convert_Object def convert_Array(o, scope, undef_value) o.map {|x| convert2(x, scope, undef_value) } end alias :convert2_Array :convert_Array def convert_Hash(o, scope, undef_value) result = {} o.each {|k,v| result[convert2(k, scope, undef_value)] = convert2(v, scope, undef_value) } result end alias :convert2_Hash :convert_Hash def convert_Regexp(o, scope, undef_value) # Puppet 3x cannot handle parameter values that are reqular expressions. Turn into regexp string in # source form o.inspect end alias :convert2_Regexp :convert_Regexp def convert_Symbol(o, scope, undef_value) case o # Support :undef since it may come from a 3x structure when :undef undef_value # 3x wants undef as either empty string or :undef else o # :default, and all others are verbatim since they are new in future evaluator end end # The :undef symbol should not be converted when nested in arrays or hashes def convert2_Symbol(o, scope, undef_value) o end def convert_PAnyType(o, scope, undef_value) o end alias :convert2_PAnyType :convert_PAnyType def convert_PCatalogEntryType(o, scope, undef_value) # Since 4x does not support dynamic scoping, all names are absolute and can be # used as is (with some check/transformation/mangling between absolute/relative form # due to Puppet::Resource's idiosyncratic behavior where some references must be # absolute and others cannot be. # Thus there is no need to call scope.resolve_type_and_titles to do dynamic lookup. Puppet::Resource.new(*catalog_type_to_split_type_title(o)) end alias :convert2_PCatalogEntryType :convert_PCatalogEntryType private # Produces an array with [type, title] from a PCatalogEntryType # This method is used to produce the arguments for creation of reference resource instances # (used when 3x is operating on a resource). # Ensures that resources are *not* absolute. # def catalog_type_to_split_type_title(catalog_type) split_type = catalog_type.is_a?(Puppet::Pops::Types::PType) ? catalog_type.type : catalog_type case split_type when Puppet::Pops::Types::PHostClassType class_name = split_type.class_name ['class', class_name.nil? ? nil : class_name.sub(/^::/, '')] when Puppet::Pops::Types::PResourceType type_name = split_type.type_name title = split_type.title if type_name =~ /^(::)?[Cc]lass/ ['class', title.nil? ? nil : title.sub(/^::/, '')] else # Ensure that title is '' if nil # Resources with absolute name always results in error because tagging does not support leading :: [type_name.nil? ? nil : type_name.sub(/^::/, ''), title.nil? ? '' : title] end else raise ArgumentError, "Cannot split the type #{catalog_type.class}, it represents neither a PHostClassType, nor a PResourceType." end end def extract_file_line(o) source_pos = Puppet::Pops::Utils.find_closest_positioned(o) return [nil, -1] unless source_pos [source_pos.locator.file, source_pos.line] end def find_closest_positioned(o) return nil if o.nil? || o.is_a?(Puppet::Pops::Model::Program) o.offset.nil? ? find_closest_positioned(o.eContainer) : Puppet::Pops::Adapters::SourcePosAdapter.adapt(o) end # Creates a diagnostic producer def diagnostic_producer Puppet::Pops::Validation::DiagnosticProducer.new( ExceptionRaisingAcceptor.new(), # Raises exception on all issues SeverityProducer.new(), # All issues are errors Puppet::Pops::Model::ModelLabelProvider.new()) end # Configure the severity of failures class SeverityProducer < Puppet::Pops::Validation::SeverityProducer Issues = Puppet::Pops::Issues def initialize super p = self # Issues triggering warning only if --debug is on if Puppet[:debug] p[Issues::EMPTY_RESOURCE_SPECIALIZATION] = :warning else p[Issues::EMPTY_RESOURCE_SPECIALIZATION] = :ignore end # Store config issues, ignore or warning p[Issues::RT_NO_STORECONFIGS_EXPORT] = Puppet[:storeconfigs] ? :ignore : :warning p[Issues::RT_NO_STORECONFIGS] = Puppet[:storeconfigs] ? :ignore : :warning end end # An acceptor of diagnostics that immediately raises an exception. class ExceptionRaisingAcceptor < Puppet::Pops::Validation::Acceptor def accept(diagnostic) super Puppet::Pops::IssueReporter.assert_and_report(self, {:message => "Evaluation Error:", :emit_warnings => true }) if errors? raise ArgumentError, "Internal Error: Configuration of runtime error handling wrong: should have raised exception" end end end class EvaluationError < StandardError end end diff --git a/lib/puppet/pops/types/type_calculator.rb b/lib/puppet/pops/types/type_calculator.rb index fd1487b78..9db174132 100644 --- a/lib/puppet/pops/types/type_calculator.rb +++ b/lib/puppet/pops/types/type_calculator.rb @@ -1,1699 +1,1703 @@ # The TypeCalculator can answer questions about puppet types. # # The Puppet type system is primarily based on sub-classing. When asking the type calculator to infer types from Ruby in general, it # may not provide the wanted answer; it does not for instance take module inclusions and extensions into account. In general the type # system should be unsurprising for anyone being exposed to the notion of type. The type `Data` may require a bit more explanation; this # is an abstract type that includes all scalar types, as well as Array with an element type compatible with Data, and Hash with key # compatible with scalar and elements compatible with Data. Expressed differently; Data is what you typically express using JSON (with # the exception that the Puppet type system also includes Pattern (regular expression) as a scalar. # # Inference # --------- # The `infer(o)` method infers a Puppet type for scalar Ruby objects, and for Arrays and Hashes. # The inference result is instance specific for single typed collections # and allows answering questions about its embedded type. It does not however preserve multiple types in # a collection, and can thus not answer questions like `[1,a].infer() =~ Array[Integer, String]` since the inference # computes the common type Scalar when combining Integer and String. # # The `infer_generic(o)` method infers a generic Puppet type for scalar Ruby object, Arrays and Hashes. # This inference result does not contain instance specific information; e.g. Array[Integer] where the integer # range is the generic default. Just `infer` it also combines types into a common type. # # The `infer_set(o)` method works like `infer` but preserves all type information. It does not do any # reduction into common types or ranges. This method of inference is best suited for answering questions # about an object being an instance of a type. It correctly answers: `[1,a].infer_set() =~ Array[Integer, String]` # # The `generalize!(t)` method modifies an instance specific inference result to a generic. The method mutates # the given argument. Basically, this removes string instances from String, and range from Integer and Float. # # Assignability # ------------- # The `assignable?(t1, t2)` method answers if t2 conforms to t1. The type t2 may be an instance, in which case # its type is inferred, or a type. # # Instance? # --------- # The `instance?(t, o)` method answers if the given object (instance) is an instance that is assignable to the given type. # # String # ------ # Creates a string representation of a type. # # Creation of Type instances # -------------------------- # Instance of the classes in the {Puppet::Pops::Types type model} are used to denote a specific type. It is most convenient # to use the {Puppet::Pops::Types::TypeFactory TypeFactory} when creating instances. # # @note # In general, new instances of the wanted type should be created as they are assigned to models using containment, and a # contained object can only be in one container at a time. Also, the type system may include more details in each type # instance, such as if it may be nil, be empty, contain a certain count etc. Or put differently, the puppet types are not # singletons. # # All types support `copy` which should be used when assigning a type where it is unknown if it is bound or not # to a parent type. A check can be made with `t.eContainer().nil?` # # Equality and Hash # ----------------- # Type instances are equal in terms of Ruby eql? and `==` if they describe the same type, but they are not `equal?` if they are not # the same type instance. Two types that describe the same type have identical hash - this makes them usable as hash keys. # # Types and Subclasses # -------------------- # In general, the type calculator should be used to answer questions if a type is a subtype of another (using {#assignable?}, or # {#instance?} if the question is if a given object is an instance of a given type (or is a subtype thereof). # Many of the types also have a Ruby subtype relationship; e.g. PHashType and PArrayType are both subtypes of PCollectionType, and # PIntegerType, PFloatType, PStringType,... are subtypes of PScalarType. Even if it is possible to answer certain questions about # type by looking at the Ruby class of the types this is considered an implementation detail, and such checks should in general # be performed by the type_calculator which implements the type system semantics. # # The PRuntimeType # ------------- # The PRuntimeType corresponds to a type in the runtime system (currently only supported runtime is 'ruby'). The # type has a runtime_type_name that corresponds to a Ruby Class name. # A Runtime[ruby] type can be used to describe any ruby class except for the puppet types that are specialized # (i.e. PRuntimeType should not be used for Integer, String, etc. since there are specialized types for those). # When the type calculator deals with PRuntimeTypes and checks for assignability, it determines the # "common ancestor class" of two classes. # This check is made based on the superclasses of the two classes being compared. In order to perform this, the # classes must be present (i.e. they are resolved from the string form in the PRuntimeType to a # loaded, instantiated Ruby Class). In general this is not a problem, since the question to produce the common # super type for two objects means that the classes must be present or there would have been # no instances present in the first place. If however the classes are not present, the type # calculator will fall back and state that the two types at least have Any in common. # # @see Puppet::Pops::Types::TypeFactory TypeFactory for how to create instances of types # @see Puppet::Pops::Types::TypeParser TypeParser how to construct a type instance from a String # @see Puppet::Pops::Types Types for details about the type model # # Using the Type Calculator # ----- # The type calculator can be directly used via its class methods. If doing time critical work and doing many # calls to the type calculator, it is more performant to create an instance and invoke the corresponding # instance methods. Note that inference is an expensive operation, rather than inferring the same thing # several times, it is in general better to infer once and then copy the result if mutation to a more generic form is # required. # # @api public # class Puppet::Pops::Types::TypeCalculator Types = Puppet::Pops::Types TheInfinity = 1.0 / 0.0 # because the Infinity symbol is not defined # @api public def self.assignable?(t1, t2) singleton.assignable?(t1,t2) end # Answers, does the given callable accept the arguments given in args (an array or a tuple) # @param callable [Puppet::Pops::Types::PCallableType] - the callable # @param args [Puppet::Pops::Types::PArrayType, Puppet::Pops::Types::PTupleType] args optionally including a lambda callable at the end # @return [Boolan] true if the callable accepts the arguments # # @api public def self.callable?(callable, args) singleton.callable?(callable, args) end # Produces a String representation of the given type. # @param t [Puppet::Pops::Types::PAnyType] the type to produce a string form # @return [String] the type in string form # # @api public # def self.string(t) singleton.string(t) end # @api public def self.infer(o) singleton.infer(o) end # @api public def self.generalize!(o) singleton.generalize!(o) end # @api public def self.infer_set(o) singleton.infer_set(o) end # @api public def self.debug_string(t) singleton.debug_string(t) end # @api public def self.enumerable(t) singleton.enumerable(t) end # @api private def self.singleton() @tc_instance ||= new end # @api public # def initialize @@assignable_visitor ||= Puppet::Pops::Visitor.new(nil,"assignable",1,1) @@infer_visitor ||= Puppet::Pops::Visitor.new(nil,"infer",0,0) @@infer_set_visitor ||= Puppet::Pops::Visitor.new(nil,"infer_set",0,0) @@instance_of_visitor ||= Puppet::Pops::Visitor.new(nil,"instance_of",1,1) @@string_visitor ||= Puppet::Pops::Visitor.new(nil,"string",0,0) @@inspect_visitor ||= Puppet::Pops::Visitor.new(nil,"debug_string",0,0) @@enumerable_visitor ||= Puppet::Pops::Visitor.new(nil,"enumerable",0,0) @@extract_visitor ||= Puppet::Pops::Visitor.new(nil,"extract",0,0) @@generalize_visitor ||= Puppet::Pops::Visitor.new(nil,"generalize",0,0) @@callable_visitor ||= Puppet::Pops::Visitor.new(nil,"callable",1,1) da = Types::PArrayType.new() da.element_type = Types::PDataType.new() @data_array = da h = Types::PHashType.new() h.element_type = Types::PDataType.new() h.key_type = Types::PScalarType.new() @data_hash = h @data_t = Types::PDataType.new() @scalar_t = Types::PScalarType.new() @numeric_t = Types::PNumericType.new() @t = Types::PAnyType.new() # Data accepts a Tuple that has 0-infinity Data compatible entries (e.g. a Tuple equivalent to Array). data_tuple = Types::PTupleType.new() data_tuple.addTypes(Types::PDataType.new()) data_tuple.size_type = Types::PIntegerType.new() data_tuple.size_type.from = 0 data_tuple.size_type.to = nil # infinity @data_tuple_t = data_tuple # Variant type compatible with Data data_variant = Types::PVariantType.new() data_variant.addTypes(@data_hash.copy) data_variant.addTypes(@data_array.copy) data_variant.addTypes(Types::PScalarType.new) data_variant.addTypes(Types::PNilType.new) data_variant.addTypes(@data_tuple_t.copy) @data_variant_t = data_variant collection_default_size = Types::PIntegerType.new() collection_default_size.from = 0 collection_default_size.to = nil # infinity @collection_default_size_t = collection_default_size non_empty_string = Types::PStringType.new non_empty_string.size_type = Types::PIntegerType.new() non_empty_string.size_type.from = 1 non_empty_string.size_type.to = nil # infinity @non_empty_string_t = non_empty_string @nil_t = Types::PNilType.new end # Convenience method to get a data type for comparisons # @api private the returned value may not be contained in another element # def data @data_t end # Convenience method to get a variant compatible with the Data type. # @api private the returned value may not be contained in another element # def data_variant @data_variant_t end def self.data_variant singleton.data_variant end # Answers the question 'is it possible to inject an instance of the given class' # A class is injectable if it has a special *assisted inject* class method called `inject` taking # an injector and a scope as argument, or if it has a zero args `initialize` method. # # @param klazz [Class, PRuntimeType] the class/type to check if it is injectable # @return [Class, nil] the injectable Class, or nil if not injectable # @api public # def injectable_class(klazz) # Handle case when we get a PType instead of a class if klazz.is_a?(Types::PRuntimeType) klazz = Puppet::Pops::Types::ClassLoader.provide(klazz) end # data types can not be injected (check again, it is not safe to assume that given RubyRuntime klazz arg was ok) return false unless type(klazz).is_a?(Types::PRuntimeType) if (klazz.respond_to?(:inject) && klazz.method(:inject).arity() == -4) || klazz.instance_method(:initialize).arity() == 0 klazz else nil end end # Answers 'can an instance of type t2 be assigned to a variable of type t'. # Does not accept nil/undef unless the type accepts it. # # @api public # def assignable?(t, t2) if t.is_a?(Class) t = type(t) end if t2.is_a?(Class) t2 = type(t2) end # Unit can be assigned to anything return true if t2.class == Types::PUnitType @@assignable_visitor.visit_this_1(self, t, t2) end # Returns an enumerable if the t represents something that can be iterated def enumerable(t) @@enumerable_visitor.visit_this_0(self, t) end # Answers, does the given callable accept the arguments given in args (an array or a tuple) # def callable?(callable, args) return false if !self.class.is_kind_of_callable?(callable) # Note that polymorphism is for the args type, the callable is always a callable @@callable_visitor.visit_this_1(self, args, callable) end # Answers if the two given types describe the same type def equals(left, right) return false unless left.is_a?(Types::PAnyType) && right.is_a?(Types::PAnyType) # Types compare per class only - an extra test must be made if the are mutually assignable # to find all types that represent the same type of instance # left == right || (assignable?(right, left) && assignable?(left, right)) end # Answers 'what is the Puppet Type corresponding to the given Ruby class' # @param c [Class] the class for which a puppet type is wanted # @api public # def type(c) raise ArgumentError, "Argument must be a Class" unless c.is_a? Class # Can't use a visitor here since we don't have an instance of the class case when c <= Integer type = Types::PIntegerType.new() when c == Float type = Types::PFloatType.new() when c == Numeric type = Types::PNumericType.new() when c == String type = Types::PStringType.new() when c == Regexp type = Types::PRegexpType.new() when c == NilClass type = Types::PNilType.new() when c == FalseClass, c == TrueClass type = Types::PBooleanType.new() when c == Class type = Types::PType.new() when c == Array # Assume array of data values type = Types::PArrayType.new() type.element_type = Types::PDataType.new() when c == Hash # Assume hash with scalar keys and data values type = Types::PHashType.new() type.key_type = Types::PScalarType.new() type.element_type = Types::PDataType.new() else type = Types::PRuntimeType.new(:runtime => :ruby, :runtime_type_name => c.name) end type end # Generalizes value specific types. The given type is mutated and returned. # @api public def generalize!(o) @@generalize_visitor.visit_this_0(self, o) o.eAllContents.each { |x| @@generalize_visitor.visit_this_0(self, x) } o end def generalize_Object(o) # do nothing, there is nothing to change for most types end def generalize_PStringType(o) o.values = [] o.size_type = nil [] end def generalize_PCollectionType(o) # erase the size constraint from Array and Hash (if one exists, it is transformed to -Infinity - + Infinity, which is # not desirable. o.size_type = nil end def generalize_PFloatType(o) o.to = nil o.from = nil end def generalize_PIntegerType(o) o.to = nil o.from = nil end # Answers 'what is the single common Puppet Type describing o', or if o is an Array or Hash, what is the # single common type of the elements (or keys and elements for a Hash). # @api public # def infer(o) @@infer_visitor.visit_this_0(self, o) end def infer_generic(o) result = generalize!(infer(o)) result end # Answers 'what is the set of Puppet Types of o' # @api public # def infer_set(o) @@infer_set_visitor.visit_this_0(self, o) end def instance_of(t, o) @@instance_of_visitor.visit_this_1(self, t, o) end def instance_of_Object(t, o) # Undef is Undef and Any, but nothing else when checking instance? return false if (o.nil?) && t.class != Types::PAnyType assignable?(t, infer(o)) end # Anything is an instance of Unit # @api private def instance_of_PUnitType(t, o) true end def instance_of_PArrayType(t, o) return false unless o.is_a?(Array) return false unless o.all? {|element| instance_of(t.element_type, element) } size_t = t.size_type || @collection_default_size_t size_t2 = size_as_type(o) # optimize by calling directly assignable_PIntegerType(size_t, size_t2) end def instance_of_PTupleType(t, o) return false unless o.is_a?(Array) # compute the tuple's min/max size, and check if that size matches size_t = t.size_type || Puppet::Pops::Types::TypeFactory.range(*t.size_range) # compute the array's size as type size_t2 = size_as_type(o) return false unless assignable?(size_t, size_t2) o.each_with_index do |element, index| return false unless instance_of(t.types[index] || t.types[-1], element) end true end def instance_of_PStructType(t, o) return false unless o.is_a?(Hash) h = t.hashed_elements # all keys must be present and have a value (even if nil/undef) (o.keys - h.keys).empty? && h.all? { |k,v| instance_of(v, o[k]) } end def instance_of_PHashType(t, o) return false unless o.is_a?(Hash) key_t = t.key_type element_t = t.element_type return false unless o.keys.all? {|key| instance_of(key_t, key) } && o.values.all? {|value| instance_of(element_t, value) } size_t = t.size_type || @collection_default_size_t size_t2 = size_as_type(o) # optimize by calling directly assignable_PIntegerType(size_t, size_t2) end def instance_of_PDataType(t, o) instance_of(@data_variant_t, o) end def instance_of_PNilType(t, o) - return o.nil? + o.nil? || o == :undef end def instance_of_POptionalType(t, o) - return true if (o.nil?) - instance_of(t.optional_type, o) + instance_of_PNilType(t, o) || instance_of(t.optional_type, o) end def instance_of_PVariantType(t, o) # instance of variant if o is instance? of any of variant's types t.types.any? { |option_t| instance_of(option_t, o) } end # Answers 'is o an instance of type t' # @api public # def self.instance?(t, o) singleton.instance_of(t,o) end # Answers 'is o an instance of type t' # @api public # def instance?(t, o) instance_of(t,o) end # Answers if t is a puppet type # @api public # def is_ptype?(t) return t.is_a?(Types::PAnyType) end # Answers if t represents the puppet type PNilType # @api public # def is_pnil?(t) return t.nil? || t.is_a?(Types::PNilType) end # Answers, 'What is the common type of t1 and t2?' # # TODO: The current implementation should be optimized for performance # # @api public # def common_type(t1, t2) raise ArgumentError, 'two types expected' unless (is_ptype?(t1) || is_pnil?(t1)) && (is_ptype?(t2) || is_pnil?(t2)) # TODO: This is not right since Scalar U Undef is Any # if either is nil, the common type is the other if is_pnil?(t1) return t2 elsif is_pnil?(t2) return t1 end # If either side is Unit, it is the other type if t1.is_a?(Types::PUnitType) return t2 elsif t2.is_a?(Types::PUnitType) return t1 end # Simple case, one is assignable to the other if assignable?(t1, t2) return t1 elsif assignable?(t2, t1) return t2 end # when both are arrays, return an array with common element type if t1.is_a?(Types::PArrayType) && t2.is_a?(Types::PArrayType) type = Types::PArrayType.new() type.element_type = common_type(t1.element_type, t2.element_type) return type end # when both are hashes, return a hash with common key- and element type if t1.is_a?(Types::PHashType) && t2.is_a?(Types::PHashType) type = Types::PHashType.new() type.key_type = common_type(t1.key_type, t2.key_type) type.element_type = common_type(t1.element_type, t2.element_type) return type end # when both are host-classes, reduce to PHostClass[] (since one was not assignable to the other) if t1.is_a?(Types::PHostClassType) && t2.is_a?(Types::PHostClassType) return Types::PHostClassType.new() end # when both are resources, reduce to Resource[T] or Resource[] (since one was not assignable to the other) if t1.is_a?(Types::PResourceType) && t2.is_a?(Types::PResourceType) result = Types::PResourceType.new() # only Resource[] unless the type name is the same if t1.type_name == t2.type_name then result.type_name = t1.type_name end # the cross assignability test above has already determined that they do not have the same type and title return result end # Integers have range, expand the range to the common range if t1.is_a?(Types::PIntegerType) && t2.is_a?(Types::PIntegerType) t1range = from_to_ordered(t1.from, t1.to) t2range = from_to_ordered(t2.from, t2.to) t = Types::PIntegerType.new() from = [t1range[0], t2range[0]].min to = [t1range[1], t2range[1]].max t.from = from unless from == TheInfinity t.to = to unless to == TheInfinity return t end # Floats have range, expand the range to the common range if t1.is_a?(Types::PFloatType) && t2.is_a?(Types::PFloatType) t1range = from_to_ordered(t1.from, t1.to) t2range = from_to_ordered(t2.from, t2.to) t = Types::PFloatType.new() from = [t1range[0], t2range[0]].min to = [t1range[1], t2range[1]].max t.from = from unless from == TheInfinity t.to = to unless to == TheInfinity return t end if t1.is_a?(Types::PStringType) && t2.is_a?(Types::PStringType) t = Types::PStringType.new() t.values = t1.values | t2.values return t end if t1.is_a?(Types::PPatternType) && t2.is_a?(Types::PPatternType) t = Types::PPatternType.new() # must make copies since patterns are contained types, not data-types t.patterns = (t1.patterns | t2.patterns).map(&:copy) return t end if t1.is_a?(Types::PEnumType) && t2.is_a?(Types::PEnumType) # The common type is one that complies with either set t = Types::PEnumType.new t.values = t1.values | t2.values return t end if t1.is_a?(Types::PVariantType) && t2.is_a?(Types::PVariantType) # The common type is one that complies with either set t = Types::PVariantType.new t.types = (t1.types | t2.types).map(&:copy) return t end if t1.is_a?(Types::PRegexpType) && t2.is_a?(Types::PRegexpType) # if they were identical, the general rule would return a parameterized regexp # since they were not, the result is a generic regexp type return Types::PPatternType.new() end if t1.is_a?(Types::PCallableType) && t2.is_a?(Types::PCallableType) # They do not have the same signature, and one is not assignable to the other, # what remains is the most general form of Callable return Types::PCallableType.new() end # Common abstract types, from most specific to most general if common_numeric?(t1, t2) return Types::PNumericType.new() end if common_scalar?(t1, t2) return Types::PScalarType.new() end if common_data?(t1,t2) return Types::PDataType.new() end # Meta types Type[Integer] + Type[String] => Type[Data] if t1.is_a?(Types::PType) && t2.is_a?(Types::PType) type = Types::PType.new() type.type = common_type(t1.type, t2.type) return type end # If both are Runtime types if t1.is_a?(Types::PRuntimeType) && t2.is_a?(Types::PRuntimeType) if t1.runtime == t2.runtime && t1.runtime_type_name == t2.runtime_type_name return t1 end # finding the common super class requires that names are resolved to class # NOTE: This only supports runtime type of :ruby c1 = Types::ClassLoader.provide_from_type(t1) c2 = Types::ClassLoader.provide_from_type(t2) if c1 && c2 c2_superclasses = superclasses(c2) superclasses(c1).each do|c1_super| c2_superclasses.each do |c2_super| if c1_super == c2_super return Types::PRuntimeType.new(:runtime => :ruby, :runtime_type_name => c1_super.name) end end end end end # They better both be Any type, or the wrong thing was asked and nil is returned if t1.is_a?(Types::PAnyType) && t2.is_a?(Types::PAnyType) return Types::PAnyType.new() end end # Produces the superclasses of the given class, including the class def superclasses(c) result = [c] while s = c.superclass result << s c = s end result end # Produces a string representing the type # @api public # def string(t) @@string_visitor.visit_this_0(self, t) end # Produces a debug string representing the type (possibly with more information that the regular string format) # @api public # def debug_string(t) @@inspect_visitor.visit_this_0(self, t) end # Reduces an enumerable of types to a single common type. # @api public # def reduce_type(enumerable) enumerable.reduce(nil) {|memo, t| common_type(memo, t) } end # Reduce an enumerable of objects to a single common type # @api public # def infer_and_reduce_type(enumerable) reduce_type(enumerable.collect() {|o| infer(o) }) end # The type of all classes is PType # @api private # def infer_Class(o) Types::PType.new() end # @api private def infer_Closure(o) o.type() end # @api private def infer_Function(o) o.class.dispatcher.to_type end # @api private def infer_Object(o) Types::PRuntimeType.new(:runtime => :ruby, :runtime_type_name => o.class.name) end # The type of all types is PType # @api private # def infer_PAnyType(o) type = Types::PType.new() type.type = o.copy type end # The type of all types is PType # This is the metatype short circuit. # @api private # def infer_PType(o) type = Types::PType.new() type.type = o.copy type end # @api private def infer_String(o) t = Types::PStringType.new() t.addValues(o) t.size_type = size_as_type(o) t end # @api private def infer_Float(o) t = Types::PFloatType.new() t.from = o t.to = o t end # @api private def infer_Integer(o) t = Types::PIntegerType.new() t.from = o t.to = o t end # @api private def infer_Regexp(o) t = Types::PRegexpType.new() t.pattern = o.source t end # @api private def infer_NilClass(o) Types::PNilType.new() end # Inference of :default as PDefaultType, and all other are Ruby[Symbol] # @api private def infer_Symbol(o) case o when :default Types::PDefaultType.new() - else infer_Object(o) end end # @api private def infer_TrueClass(o) Types::PBooleanType.new() end # @api private def infer_FalseClass(o) Types::PBooleanType.new() end # @api private # A Puppet::Parser::Resource, or Puppet::Resource # def infer_Resource(o) t = Types::PResourceType.new() t.type_name = o.type.to_s.downcase # Only Puppet::Resource can have a title that is a symbol :undef, a PResource cannot. # A mapping must be made to empty string. A nil value will result in an error later title = o.title t.title = (:undef == title ? '' : title) type = Types::PType.new() type.type = t type end # @api private def infer_Array(o) type = Types::PArrayType.new() type.element_type = if o.empty? Types::PNilType.new() else infer_and_reduce_type(o) end type.size_type = size_as_type(o) type end # @api private def infer_Hash(o) type = Types::PHashType.new() if o.empty? ktype = Types::PNilType.new() etype = Types::PNilType.new() else ktype = infer_and_reduce_type(o.keys()) etype = infer_and_reduce_type(o.values()) end type.key_type = ktype type.element_type = etype type.size_type = size_as_type(o) type end def size_as_type(collection) size = collection.size t = Types::PIntegerType.new() t.from = size t.to = size t end # Common case for everything that intrinsically only has a single type def infer_set_Object(o) infer(o) end def infer_set_Array(o) if o.empty? type = Types::PArrayType.new() type.element_type = Types::PNilType.new() type.size_type = size_as_type(o) else type = Types::PTupleType.new() type.types = o.map() {|x| infer_set(x) } end type end def infer_set_Hash(o) type = Types::PHashType.new() if o.empty? ktype = Types::PNilType.new() vtype = Types::PNilType.new() else ktype = Types::PVariantType.new() ktype.types = o.keys.map() {|k| infer_set(k) } etype = Types::PVariantType.new() etype.types = o.values.map() {|e| infer_set(e) } end type.key_type = unwrap_single_variant(ktype) type.element_type = unwrap_single_variant(etype) type.size_type = size_as_type(o) type end def unwrap_single_variant(possible_variant) if possible_variant.is_a?(Types::PVariantType) && possible_variant.types.size == 1 possible_variant.types[0] else possible_variant end end # False in general type calculator # @api private def assignable_Object(t, t2) false end # @api private def assignable_PAnyType(t, t2) t2.is_a?(Types::PAnyType) end # @api private def assignable_PNilType(t, t2) # Only undef/nil is assignable to nil type t2.is_a?(Types::PNilType) end # Anything is assignable to a Unit type # @api private def assignable_PUnitType(t, t2) true end # @api private def assignable_PDefaultType(t, t2) # Only default is assignable to default type t2.is_a?(Types::PDefaultType) end # @api private def assignable_PScalarType(t, t2) t2.is_a?(Types::PScalarType) end # @api private def assignable_PNumericType(t, t2) t2.is_a?(Types::PNumericType) end # @api private def assignable_PIntegerType(t, t2) return false unless t2.is_a?(Types::PIntegerType) trange = from_to_ordered(t.from, t.to) t2range = from_to_ordered(t2.from, t2.to) # If t2 min and max are within the range of t trange[0] <= t2range[0] && trange[1] >= t2range[1] end # Transform int range to a size constraint # if range == nil the constraint is 1,1 # if range.from == nil min size = 1 # if range.to == nil max size == Infinity # def size_range(range) return [1,1] if range.nil? from = range.from to = range.to x = from.nil? ? 1 : from y = to.nil? ? TheInfinity : to if x < y [x, y] else [y, x] end end # @api private def from_to_ordered(from, to) x = (from.nil? || from == :default) ? -TheInfinity : from y = (to.nil? || to == :default) ? TheInfinity : to if x < y [x, y] else [y, x] end end # @api private def assignable_PVariantType(t, t2) # Data is a specific variant t2 = @data_variant_t if t2.is_a?(Types::PDataType) if t2.is_a?(Types::PVariantType) # A variant is assignable if all of its options are assignable to one of this type's options return true if t == t2 t2.types.all? do |other| # if the other is a Variant, all of its options, but be assignable to one of this type's options other = other.is_a?(Types::PDataType) ? @data_variant_t : other if other.is_a?(Types::PVariantType) assignable?(t, other) else t.types.any? {|option_t| assignable?(option_t, other) } end end else # A variant is assignable if t2 is assignable to any of its types t.types.any? { |option_t| assignable?(option_t, t2) } end end # Catch all not callable combinations def callable_Object(o, callable_t) false end def callable_PTupleType(args_tuple, callable_t) if args_tuple.size_type raise ArgumentError, "Callable tuple may not have a size constraint when used as args" end # Assume no block was given - i.e. it is nil, and its type is PNilType block_t = @nil_t if self.class.is_kind_of_callable?(args_tuple.types.last) # a split is needed to make it possible to use required, optional, and varargs semantics # of the tuple type. # args_tuple = args_tuple.copy # to drop the callable, it must be removed explicitly since this is an rgen array args_tuple.removeTypes(block_t = args_tuple.types.last()) else # no block was given, if it is required, the below will fail end # unless argument types match parameter types return false unless assignable?(callable_t.param_types, args_tuple) # can the given block be *called* with a signature requirement specified by callable_t? assignable?(callable_t.block_type || @nil_t, block_t) end # @api private def self.is_kind_of_callable?(t, optional = true) case t when Types::PCallableType true when Types::POptionalType optional && is_kind_of_callable?(t.optional_type, optional) when Types::PVariantType t.types.all? {|t2| is_kind_of_callable?(t2, optional) } else false end end def callable_PArrayType(args_array, callable_t) return false unless assignable?(callable_t.param_types, args_array) # does not support calling with a block, but have to check that callable is ok with missing block assignable?(callable_t.block_type || @nil_t, @nil_t) end def callable_PNilType(nil_t, callable_t) # if callable_t is Optional (or indeed PNilType), this means that 'missing callable' is accepted assignable?(callable_t, nil_t) end def callable_PCallableType(given_callable_t, required_callable_t) # If the required callable is euqal or more specific than the given, the given is callable assignable?(required_callable_t, given_callable_t) end def max(a,b) a >=b ? a : b end def min(a,b) a <= b ? a : b end def assignable_PTupleType(t, t2) return true if t == t2 || t.types.empty? && (t2.is_a?(Types::PArrayType)) size_t = t.size_type || Puppet::Pops::Types::TypeFactory.range(*t.size_range) if t2.is_a?(Types::PTupleType) size_t2 = t2.size_type || Puppet::Pops::Types::TypeFactory.range(*t2.size_range) # not assignable if the number of types in t2 is outside number of types in t1 if assignable?(size_t, size_t2) t2.types.size.times do |index| return false unless assignable?((t.types[index] || t.types[-1]), t2.types[index]) end return true else return false end elsif t2.is_a?(Types::PArrayType) t2_entry = t2.element_type # Array of anything can not be assigned (unless tuple is tuple of anything) - this case # was handled at the top of this method. # return false if t2_entry.nil? size_t = t.size_type || Puppet::Pops::Types::TypeFactory.range(*t.size_range) size_t2 = t2.size_type || @collection_default_size_t return false unless assignable?(size_t, size_t2) min(t.types.size, size_t2.range()[1]).times do |index| return false unless assignable?((t.types[index] || t.types[-1]), t2_entry) end true else false end end # Produces the tuple entry at the given index given a tuple type, its from/to constraints on the last # type, and an index. # Produces nil if the index is out of bounds # from must be less than to, and from may not be less than 0 # # @api private # def tuple_entry_at(tuple_t, from, to, index) regular = (tuple_t.types.size - 1) if index < regular tuple_t.types[index] elsif index < regular + to # in the varargs part tuple_t.types[-1] else nil end end # @api private # def assignable_PStructType(t, t2) return true if t == t2 || t.elements.empty? && (t2.is_a?(Types::PHashType)) h = t.hashed_elements if t2.is_a?(Types::PStructType) h2 = t2.hashed_elements h.size == h2.size && h.all? {|k, v| assignable?(v, h2[k]) } elsif t2.is_a?(Types::PHashType) size_t2 = t2.size_type || @collection_default_size_t size_t = Types::PIntegerType.new size_t.from = size_t.to = h.size # compatible size # hash key type must be string of min 1 size # hash value t must be assignable to each key element_type = t2.element_type assignable_PIntegerType(size_t, size_t2) && assignable?(@non_empty_string_t, t2.key_type) && h.all? {|k,v| assignable?(v, element_type) } else false end end # @api private def assignable_POptionalType(t, t2) return true if t2.is_a?(Types::PNilType) if t2.is_a?(Types::POptionalType) assignable?(t.optional_type, t2.optional_type) else assignable?(t.optional_type, t2) end end # @api private def assignable_PEnumType(t, t2) - return true if t == t2 || (t.values.empty? && (t2.is_a?(Types::PStringType) || t2.is_a?(Types::PEnumType))) + return true if t == t2 + if t.values.empty? + return true if t2.is_a?(Types::PStringType) || t2.is_a?(Types::PEnumType) || t2.is_a?(Types::PPatternType) + end case t2 when Types::PStringType # if the set of strings are all found in the set of enums !t2.values.empty?() && t2.values.all? { |s| t.values.any? { |e| e == s }} when Types::PVariantType t2.types.all? {|variant_t| assignable_PEnumType(t, variant_t) } when Types::PEnumType # empty means any enum return true if t.values.empty? !t2.values.empty? && t2.values.all? { |s| t.values.any? {|e| e == s }} else false end end # @api private def assignable_PStringType(t, t2) if t.values.empty? # A general string is assignable by any other string or pattern restricted string # if the string has a size constraint it does not match since there is no reasonable way # to compute the min/max length a pattern will match. For enum, it is possible to test that # each enumerator value is within range size_t = t.size_type || @collection_default_size_t case t2 when Types::PStringType # true if size compliant size_t2 = t2.size_type || @collection_default_size_t assignable_PIntegerType(size_t, size_t2) when Types::PPatternType # true if size constraint is at least 0 to +Infinity (which is the same as the default) assignable_PIntegerType(size_t, @collection_default_size_t) when Types::PEnumType if t2.values && !t2.values.empty? # true if all enum values are within range min, max = t2.values.map(&:size).minmax trange = from_to_ordered(size_t.from, size_t.to) t2range = [min, max] # If t2 min and max are within the range of t trange[0] <= t2range[0] && trange[1] >= t2range[1] else # enum represents all enums, and thus all strings, a sized constrained string can thus not # be assigned any enum (unless it is max size). assignable_PIntegerType(size_t, @collection_default_size_t) end else # no other type matches string false end elsif t2.is_a?(Types::PStringType) # A specific string acts as a set of strings - must have exactly the same strings # In this case, size does not matter since the definition is very precise anyway Set.new(t.values) == Set.new(t2.values) else # All others are false, since no other type describes the same set of specific strings false end end # @api private def assignable_PPatternType(t, t2) return true if t == t2 case t2 when Types::PStringType, Types::PEnumType values = t2.values when Types::PVariantType return t2.types.all? {|variant_t| assignable_PPatternType(t, variant_t) } + when Types::PPatternType + return t.patterns.empty? ? true : false else return false end if t2.values.empty? # Strings / Enums (unknown which ones) cannot all match a pattern, but if there is no pattern it is ok # (There should really always be a pattern, but better safe than sorry). return t.patterns.empty? ? true : false end - # all strings in String/Enum type must match one of the patterns in Pattern type + # all strings in String/Enum type must match one of the patterns in Pattern type, + # or Pattern represents all Patterns == all Strings regexps = t.patterns.map {|p| p.regexp } - t2.values.all? { |v| regexps.any? {|re| re.match(v) } } + regexps.empty? || t2.values.all? { |v| regexps.any? {|re| re.match(v) } } end # @api private def assignable_PFloatType(t, t2) return false unless t2.is_a?(Types::PFloatType) trange = from_to_ordered(t.from, t.to) t2range = from_to_ordered(t2.from, t2.to) # If t2 min and max are within the range of t trange[0] <= t2range[0] && trange[1] >= t2range[1] end # @api private def assignable_PBooleanType(t, t2) t2.is_a?(Types::PBooleanType) end # @api private def assignable_PRegexpType(t, t2) t2.is_a?(Types::PRegexpType) && (t.pattern.nil? || t.pattern == t2.pattern) end # @api private def assignable_PCallableType(t, t2) return false unless t2.is_a?(Types::PCallableType) # nil param_types means, any other Callable is assignable return true if t.param_types.nil? # NOTE: these tests are made in reverse as it is calling the callable that is constrained # (it's lower bound), not its upper bound return false unless assignable?(t2.param_types, t.param_types) # names are ignored, they are just information # Blocks must be compatible this_block_t = t.block_type || @nil_t that_block_t = t2.block_type || @nil_t assignable?(that_block_t, this_block_t) end # @api private def assignable_PCollectionType(t, t2) size_t = t.size_type || @collection_default_size_t case t2 when Types::PCollectionType size_t2 = t2.size_type || @collection_default_size_t assignable_PIntegerType(size_t, size_t2) when Types::PTupleType # compute the tuple's min/max size, and check if that size matches from, to = size_range(t2.size_type) t2s = Types::PIntegerType.new() t2s.from = t2.types.size - 1 + from t2s.to = t2.types.size - 1 + to assignable_PIntegerType(size_t, t2s) when Types::PStructType from = to = t2.elements.size t2s = Types::PIntegerType.new() t2s.from = from t2s.to = to assignable_PIntegerType(size_t, t2s) else false end end # @api private def assignable_PType(t, t2) return false unless t2.is_a?(Types::PType) return true if t.type.nil? # wide enough to handle all types return false if t2.type.nil? # wider than t assignable?(t.type, t2.type) end # Array is assignable if t2 is an Array and t2's element type is assignable, or if t2 is a Tuple # where # @api private def assignable_PArrayType(t, t2) if t2.is_a?(Types::PArrayType) return false unless assignable?(t.element_type, t2.element_type) assignable_PCollectionType(t, t2) elsif t2.is_a?(Types::PTupleType) return false unless t2.types.all? {|t2_element| assignable?(t.element_type, t2_element) } t2_regular = t2.types[0..-2] t2_ranged = t2.types[-1] t2_from, t2_to = size_range(t2.size_type) t2_required = t2_regular.size + t2_from t_entry = t.element_type # Tuple of anything can not be assigned (unless array is tuple of anything) - this case # was handled at the top of this method. # return false if t_entry.nil? # array type may be size constrained size_t = t.size_type || @collection_default_size_t min, max = size_t.range # Tuple with fewer min entries can not be assigned return false if t2_required < min # Tuple with more optionally available entries can not be assigned return false if t2_regular.size + t2_to > max # each tuple type must be assignable to the element type t2_required.times do |index| t2_entry = tuple_entry_at(t2, t2_from, t2_to, index) return false unless assignable?(t_entry, t2_entry) end # ... and so must the last, possibly optional (ranged) type return assignable?(t_entry, t2_ranged) else false end end # Hash is assignable if t2 is a Hash and t2's key and element types are assignable # @api private def assignable_PHashType(t, t2) case t2 when Types::PHashType return false unless assignable?(t.key_type, t2.key_type) && assignable?(t.element_type, t2.element_type) assignable_PCollectionType(t, t2) when Types::PStructType # hash must accept String as key type # hash must accept all value types # hash must accept the size of the struct size_t = t.size_type || @collection_default_size_t min, max = size_t.range struct_size = t2.elements.size element_type = t.element_type ( struct_size >= min && struct_size <= max && assignable?(t.key_type, @non_empty_string_t) && t2.hashed_elements.all? {|k,v| assignable?(element_type, v) }) else false end end # @api private def assignable_PCatalogEntryType(t1, t2) t2.is_a?(Types::PCatalogEntryType) end # @api private def assignable_PHostClassType(t1, t2) return false unless t2.is_a?(Types::PHostClassType) # Class = Class[name}, Class[name] != Class return true if t1.class_name.nil? # Class[name] = Class[name] return t1.class_name == t2.class_name end # @api private def assignable_PResourceType(t1, t2) return false unless t2.is_a?(Types::PResourceType) return true if t1.type_name.nil? return false if t1.type_name != t2.type_name return true if t1.title.nil? return t1.title == t2.title end # Data is assignable by other Data and by Array[Data] and Hash[Scalar, Data] # @api private def assignable_PDataType(t, t2) t2.is_a?(Types::PDataType) || assignable?(@data_variant_t, t2) end # Assignable if t2's has the same runtime and the runtime name resolves to # a class that is the same or subclass of t1's resolved runtime type name # @api private def assignable_PRuntimeType(t1, t2) return false unless t2.is_a?(Types::PRuntimeType) return false unless t1.runtime == t2.runtime return true if t1.runtime_type_name.nil? # t1 is wider return false if t2.runtime_type_name.nil? # t1 not nil, so t2 can not be wider # NOTE: This only supports Ruby, must change when/if the set of runtimes is expanded c1 = class_from_string(t1.runtime_type_name) c2 = class_from_string(t2.runtime_type_name) return false unless c1.is_a?(Class) && c2.is_a?(Class) !!(c2 <= c1) end # @api private def debug_string_Object(t) string(t) end # @api private def string_PType(t) if t.type.nil? "Type" else "Type[#{string(t.type)}]" end end # @api private def string_NilClass(t) ; '?' ; end # @api private def string_String(t) ; t ; end # @api private def string_Symbol(t) ; t.to_s ; end def string_PAnyType(t) ; "Any" ; end # @api private def string_PNilType(t) ; 'Undef' ; end # @api private def string_PDefaultType(t) ; 'Default' ; end # @api private def string_PBooleanType(t) ; "Boolean" ; end # @api private def string_PScalarType(t) ; "Scalar" ; end # @api private def string_PDataType(t) ; "Data" ; end # @api private def string_PNumericType(t) ; "Numeric" ; end # @api private def string_PIntegerType(t) range = range_array_part(t) unless range.empty? "Integer[#{range.join(', ')}]" else "Integer" end end # Produces a string from an Integer range type that is used inside other type strings # @api private def range_array_part(t) return [] if t.nil? || (t.from.nil? && t.to.nil?) [t.from.nil? ? 'default' : t.from , t.to.nil? ? 'default' : t.to ] end # @api private def string_PFloatType(t) range = range_array_part(t) unless range.empty? "Float[#{range.join(', ')}]" else "Float" end end # @api private def string_PRegexpType(t) t.pattern.nil? ? "Regexp" : "Regexp[#{t.regexp.inspect}]" end # @api private def string_PStringType(t) # skip values in regular output - see debug_string range = range_array_part(t.size_type) unless range.empty? "String[#{range.join(', ')}]" else "String" end end # @api private def debug_string_PStringType(t) range = range_array_part(t.size_type) range_part = range.empty? ? '' : '[' << range.join(' ,') << '], ' "String[" << range_part << (t.values.map {|s| "'#{s}'" }).join(', ') << ']' end # @api private def string_PEnumType(t) return "Enum" if t.values.empty? "Enum[" << t.values.map {|s| "'#{s}'" }.join(', ') << ']' end # @api private def string_PVariantType(t) return "Variant" if t.types.empty? "Variant[" << t.types.map {|t2| string(t2) }.join(', ') << ']' end # @api private def string_PTupleType(t) range = range_array_part(t.size_type) return "Tuple" if t.types.empty? s = "Tuple[" << t.types.map {|t2| string(t2) }.join(', ') unless range.empty? s << ", " << range.join(', ') end s << "]" s end # @api private def string_PCallableType(t) # generic return "Callable" if t.param_types.nil? if t.param_types.types.empty? range = [0, 0] else range = range_array_part(t.param_types.size_type) end # translate to string, and skip Unit types types = t.param_types.types.map {|t2| string(t2) unless t2.class == Types::PUnitType }.compact s = "Callable[" << types.join(', ') unless range.empty? (s << ', ') unless types.empty? s << range.join(', ') end # Add block T last (after min, max) if present) # unless t.block_type.nil? (s << ', ') unless types.empty? && range.empty? s << string(t.block_type) end s << "]" s end # @api private def string_PStructType(t) return "Struct" if t.elements.empty? "Struct[{" << t.elements.map {|element| string(element) }.join(', ') << "}]" end def string_PStructElement(t) "'#{t.name}'=>#{string(t.type)}" end # @api private def string_PPatternType(t) return "Pattern" if t.patterns.empty? "Pattern[" << t.patterns.map {|s| "#{s.regexp.inspect}" }.join(', ') << ']' end # @api private def string_PCollectionType(t) range = range_array_part(t.size_type) unless range.empty? "Collection[#{range.join(', ')}]" else "Collection" end end # @api private def string_PUnitType(t) "Unit" end # @api private def string_PRuntimeType(t) ; "Runtime[#{string(t.runtime)}, #{string(t.runtime_type_name)}]" ; end # @api private def string_PArrayType(t) parts = [string(t.element_type)] + range_array_part(t.size_type) "Array[#{parts.join(', ')}]" end # @api private def string_PHashType(t) parts = [string(t.key_type), string(t.element_type)] + range_array_part(t.size_type) "Hash[#{parts.join(', ')}]" end # @api private def string_PCatalogEntryType(t) "CatalogEntry" end # @api private def string_PHostClassType(t) if t.class_name "Class[#{t.class_name}]" else "Class" end end # @api private def string_PResourceType(t) if t.type_name if t.title "#{capitalize_segments(t.type_name)}['#{t.title}']" else capitalize_segments(t.type_name) end else "Resource" end end def string_POptionalType(t) if t.optional_type.nil? "Optional" else "Optional[#{string(t.optional_type)}]" end end # Catches all non enumerable types # @api private def enumerable_Object(o) nil end # @api private def enumerable_PIntegerType(t) # Not enumerable if representing an infinite range return nil if t.size == TheInfinity t end def self.copy_as_tuple(t) case t when Types::PTupleType t.copy when Types::PArrayType # transform array to tuple result = Types::PTupleType.new result.addTypes(t.element_type.copy) result.size_type = t.size_type.nil? ? nil : t.size_type.copy result else raise ArgumentError, "Internal Error: Only Array and Tuple can be given to copy_as_tuple" end end private NAME_SEGMENT_SEPARATOR = '::'.freeze def capitalize_segments(s) s.split(NAME_SEGMENT_SEPARATOR).map(&:capitalize).join(NAME_SEGMENT_SEPARATOR) end def class_from_string(str) begin str.split(NAME_SEGMENT_SEPARATOR).inject(Object) do |memo, name_segment| memo.const_get(name_segment) end rescue NameError return nil end end def common_data?(t1, t2) assignable?(@data_t, t1) && assignable?(@data_t, t2) end def common_scalar?(t1, t2) assignable?(@scalar_t, t1) && assignable?(@scalar_t, t2) end def common_numeric?(t1, t2) assignable?(@numeric_t, t1) && assignable?(@numeric_t, t2) end end diff --git a/lib/puppet/provider/ssh_authorized_key/parsed.rb b/lib/puppet/provider/ssh_authorized_key/parsed.rb index 67403c9d6..a1c9ad4c7 100644 --- a/lib/puppet/provider/ssh_authorized_key/parsed.rb +++ b/lib/puppet/provider/ssh_authorized_key/parsed.rb @@ -1,89 +1,105 @@ require 'puppet/provider/parsedfile' Puppet::Type.type(:ssh_authorized_key).provide( :parsed, :parent => Puppet::Provider::ParsedFile, :filetype => :flat, :default_target => '' ) do desc "Parse and generate authorized_keys files for SSH." text_line :comment, :match => /^\s*#/ text_line :blank, :match => /^\s*$/ record_line :parsed, :fields => %w{options type key name}, :optional => %w{options}, :rts => /^\s+/, :match => Puppet::Type.type(:ssh_authorized_key).keyline_regex, :post_parse => proc { |h| h[:name] = "" if h[:name] == :absent h[:options] ||= [:absent] h[:options] = Puppet::Type::Ssh_authorized_key::ProviderParsed.parse_options(h[:options]) if h[:options].is_a? String }, :pre_gen => proc { |h| + # if this name was generated, don't write it back to disk + h[:name] = "" if h[:unnamed] h[:options] = [] if h[:options].include?(:absent) h[:options] = h[:options].join(',') } record_line :key_v1, :fields => %w{options bits exponent modulus name}, :optional => %w{options}, :rts => /^\s+/, :match => /^(?:(.+) )?(\d+) (\d+) (\d+)(?: (.+))?$/ def dir_perm 0700 end def file_perm 0600 end def user uid = Puppet::FileSystem.stat(target).uid Etc.getpwuid(uid).name end def flush raise Puppet::Error, "Cannot write SSH authorized keys without user" unless @resource.should(:user) raise Puppet::Error, "User '#{@resource.should(:user)}' does not exist" unless Puppet::Util.uid(@resource.should(:user)) # ParsedFile usually calls backup_target much later in the flush process, # but our SUID makes that fail to open filebucket files for writing. # Fortunately, there's already logic to make sure it only ever happens once, # so calling it here supresses the later attempt by our superclass's flush method. self.class.backup_target(target) Puppet::Util::SUIDManager.asuser(@resource.should(:user)) do unless Puppet::FileSystem.exist?(dir = File.dirname(target)) Puppet.debug "Creating #{dir}" Dir.mkdir(dir, dir_perm) end super File.chmod(file_perm, target) end end # parse sshv2 option strings, wich is a comma separated list of # either key="values" elements or bare-word elements def self.parse_options(options) result = [] scanner = StringScanner.new(options) while !scanner.eos? scanner.skip(/[ \t]*/) # scan a long option if out = scanner.scan(/[-a-z0-9A-Z_]+=\".*?[^\\]\"/) or out = scanner.scan(/[-a-z0-9A-Z_]+/) result << out else # found an unscannable token, let's abort break end # eat a comma scanner.skip(/[ \t]*,[ \t]*/) end result end + + def self.prefetch_hook(records) + name_index = 0 + records.each do |record| + if record[:record_type] == :parsed && record[:name].empty? + record[:unnamed] = true + # Generate a unique ID for unnamed keys, in case they need purging. + # If you change this, you have to keep + # Puppet::Type::User#unknown_keys_in_file in sync! (PUP-3357) + record[:name] = "#{record[:target]}:unnamed-#{ name_index += 1 }" + Puppet.debug("generating name for on-disk ssh_authorized_key #{record[:key]}: #{record[:name]}") + end + end + end end diff --git a/lib/puppet/settings.rb b/lib/puppet/settings.rb index 499ee1502..a0f41f1c7 100644 --- a/lib/puppet/settings.rb +++ b/lib/puppet/settings.rb @@ -1,1420 +1,1437 @@ require 'puppet' require 'getoptlong' require 'puppet/util/watched_file' require 'puppet/util/command_line/puppet_option_parser' require 'forwardable' # The class for handling configuration files. class Puppet::Settings extend Forwardable include Enumerable require 'puppet/settings/errors' require 'puppet/settings/base_setting' require 'puppet/settings/string_setting' require 'puppet/settings/enum_setting' require 'puppet/settings/array_setting' require 'puppet/settings/file_setting' require 'puppet/settings/directory_setting' require 'puppet/settings/file_or_directory_setting' require 'puppet/settings/path_setting' require 'puppet/settings/boolean_setting' require 'puppet/settings/terminus_setting' require 'puppet/settings/duration_setting' require 'puppet/settings/ttl_setting' require 'puppet/settings/priority_setting' require 'puppet/settings/autosign_setting' require 'puppet/settings/config_file' require 'puppet/settings/value_translator' require 'puppet/settings/environment_conf' # local reference for convenience PuppetOptionParser = Puppet::Util::CommandLine::PuppetOptionParser attr_accessor :files attr_reader :timer # These are the settings that every app is required to specify; there are reasonable defaults defined in application.rb. REQUIRED_APP_SETTINGS = [:logdir, :confdir, :vardir] # This method is intended for puppet internal use only; it is a convenience method that # returns reasonable application default settings values for a given run_mode. def self.app_defaults_for_run_mode(run_mode) { :name => run_mode.to_s, :run_mode => run_mode.name, :confdir => run_mode.conf_dir, :vardir => run_mode.var_dir, :rundir => run_mode.run_dir, :logdir => run_mode.log_dir, } end def self.default_certname() hostname = hostname_fact domain = domain_fact if domain and domain != "" fqdn = [hostname, domain].join(".") else fqdn = hostname end fqdn.to_s.gsub(/\.$/, '') end def self.hostname_fact() Facter["hostname"].value end def self.domain_fact() Facter["domain"].value end def self.default_config_file_name "puppet.conf" end # Create a new collection of config settings. def initialize @config = {} @shortnames = {} @created = [] # Keep track of set values. @value_sets = { :cli => Values.new(:cli, @config), :memory => Values.new(:memory, @config), :application_defaults => Values.new(:application_defaults, @config), :overridden_defaults => Values.new(:overridden_defaults, @config), } @configuration_file = nil # And keep a per-environment cache @cache = Hash.new { |hash, key| hash[key] = {} } @values = Hash.new { |hash, key| hash[key] = {} } # The list of sections we've used. @used = [] @hooks_to_call_on_application_initialization = [] @deprecated_setting_names = [] @deprecated_settings_that_have_been_configured = [] @translate = Puppet::Settings::ValueTranslator.new @config_file_parser = Puppet::Settings::ConfigFile.new(@translate) end # @param name [Symbol] The name of the setting to fetch # @return [Puppet::Settings::BaseSetting] The setting object def setting(name) @config[name] end # Retrieve a config value # @param param [Symbol] the name of the setting # @return [Object] the value of the setting # @api private def [](param) if @deprecated_setting_names.include?(param) issue_deprecation_warning(setting(param), "Accessing '#{param}' as a setting is deprecated.") end value(param) end # Set a config value. This doesn't set the defaults, it sets the value itself. # @param param [Symbol] the name of the setting # @param value [Object] the new value of the setting # @api private def []=(param, value) if @deprecated_setting_names.include?(param) issue_deprecation_warning(setting(param), "Modifying '#{param}' as a setting is deprecated.") end @value_sets[:memory].set(param, value) unsafe_flush_cache end # Create a new default value for the given setting. The default overrides are # higher precedence than the defaults given in defaults.rb, but lower # precedence than any other values for the setting. This allows one setting # `a` to change the default of setting `b`, but still allow a user to provide # a value for setting `b`. # # @param param [Symbol] the name of the setting # @param value [Object] the new default value for the setting # @api private def override_default(param, value) @value_sets[:overridden_defaults].set(param, value) unsafe_flush_cache end # Generate the list of valid arguments, in a format that GetoptLong can # understand, and add them to the passed option list. def addargs(options) # Add all of the settings as valid options. self.each { |name, setting| setting.getopt_args.each { |args| options << args } } options end # Generate the list of valid arguments, in a format that OptionParser can # understand, and add them to the passed option list. def optparse_addargs(options) # Add all of the settings as valid options. self.each { |name, setting| options << setting.optparse_args } options end # Is our setting a boolean setting? def boolean?(param) param = param.to_sym @config.include?(param) and @config[param].kind_of?(BooleanSetting) end # Remove all set values, potentially skipping cli values. def clear unsafe_clear end # Remove all set values, potentially skipping cli values. def unsafe_clear(clear_cli = true, clear_application_defaults = false) if clear_application_defaults @value_sets[:application_defaults] = Values.new(:application_defaults, @config) @app_defaults_initialized = false end if clear_cli @value_sets[:cli] = Values.new(:cli, @config) # Only clear the 'used' values if we were explicitly asked to clear out # :cli values; otherwise, it may be just a config file reparse, # and we want to retain this cli values. @used = [] end @value_sets[:memory] = Values.new(:memory, @config) @value_sets[:overridden_defaults] = Values.new(:overridden_defaults, @config) @deprecated_settings_that_have_been_configured.clear @values.clear @cache.clear end private :unsafe_clear + # Clears all cached settings for a particular environment to ensure + # that changes to environment.conf are reflected in the settings if + # the environment timeout has expired. + # + # param [String, Symbol] environment the name of environment to clear settings for + # + # @api private + def clear_environment_settings(environment) + + if environment.nil? + return + end + + @cache[environment.to_sym].clear + @values[environment.to_sym] = {} + end + # Clear @cache, @used and the Environment. # # Whenever an object is returned by Settings, a copy is stored in @cache. # As long as Setting attributes that determine the content of returned # objects remain unchanged, Settings can keep returning objects from @cache # without re-fetching or re-generating them. # # Whenever a Settings attribute changes, such as @values or @preferred_run_mode, # this method must be called to clear out the caches so that updated # objects will be returned. def flush_cache unsafe_flush_cache end def unsafe_flush_cache clearused # Clear the list of environments, because they cache, at least, the module path. # We *could* preferentially just clear them if the modulepath is changed, # but we don't really know if, say, the vardir is changed and the modulepath # is defined relative to it. We need the defined?(stuff) because of loading # order issues. Puppet::Node::Environment.clear if defined?(Puppet::Node) and defined?(Puppet::Node::Environment) end private :unsafe_flush_cache def clearused @cache.clear @used = [] end def global_defaults_initialized?() @global_defaults_initialized end def initialize_global_settings(args = []) raise Puppet::DevError, "Attempting to initialize global default settings more than once!" if global_defaults_initialized? # The first two phases of the lifecycle of a puppet application are: # 1) Parse the command line options and handle any of them that are # registered, defined "global" puppet settings (mostly from defaults.rb). # 2) Parse the puppet config file(s). parse_global_options(args) parse_config_files @global_defaults_initialized = true end # This method is called during application bootstrapping. It is responsible for parsing all of the # command line options and initializing the settings accordingly. # # It will ignore options that are not defined in the global puppet settings list, because they may # be valid options for the specific application that we are about to launch... however, at this point # in the bootstrapping lifecycle, we don't yet know what that application is. def parse_global_options(args) # Create an option parser option_parser = PuppetOptionParser.new option_parser.ignore_invalid_options = true # Add all global options to it. self.optparse_addargs([]).each do |option| option_parser.on(*option) do |arg| opt, val = Puppet::Settings.clean_opt(option[0], arg) handlearg(opt, val) end end option_parser.on('--run_mode', "The effective 'run mode' of the application: master, agent, or user.", :REQUIRED) do |arg| Puppet.settings.preferred_run_mode = arg end option_parser.parse(args) # remove run_mode options from the arguments so that later parses don't think # it is an unknown option. while option_index = args.index('--run_mode') do args.delete_at option_index args.delete_at option_index end args.reject! { |arg| arg.start_with? '--run_mode=' } end private :parse_global_options # A utility method (public, is used by application.rb and perhaps elsewhere) that munges a command-line # option string into the format that Puppet.settings expects. (This mostly has to deal with handling the # "no-" prefix on flag/boolean options). # # @param [String] opt the command line option that we are munging # @param [String, TrueClass, FalseClass] val the value for the setting (as determined by the OptionParser) def self.clean_opt(opt, val) # rewrite --[no-]option to --no-option if that's what was given if opt =~ /\[no-\]/ and !val opt = opt.gsub(/\[no-\]/,'no-') end # otherwise remove the [no-] prefix to not confuse everybody opt = opt.gsub(/\[no-\]/, '') [opt, val] end def app_defaults_initialized? @app_defaults_initialized end def initialize_app_defaults(app_defaults) REQUIRED_APP_SETTINGS.each do |key| raise SettingsError, "missing required app default setting '#{key}'" unless app_defaults.has_key?(key) end app_defaults.each do |key, value| if key == :run_mode self.preferred_run_mode = value else @value_sets[:application_defaults].set(key, value) unsafe_flush_cache end end apply_metadata call_hooks_deferred_to_application_initialization issue_deprecations @app_defaults_initialized = true end def call_hooks_deferred_to_application_initialization(options = {}) @hooks_to_call_on_application_initialization.each do |setting| begin setting.handle(self.value(setting.name)) rescue InterpolationError => err raise InterpolationError, err, err.backtrace unless options[:ignore_interpolation_dependency_errors] #swallow. We're not concerned if we can't call hooks because dependencies don't exist yet #we'll get another chance after application defaults are initialized end end end private :call_hooks_deferred_to_application_initialization # Return a value's description. def description(name) if obj = @config[name.to_sym] obj.desc else nil end end def_delegator :@config, :each # Iterate over each section name. def eachsection yielded = [] @config.each do |name, object| section = object.section unless yielded.include? section yield section yielded << section end end end # Return an object by name. def setting(param) param = param.to_sym @config[param] end # Handle a command-line argument. def handlearg(opt, value = nil) @cache.clear if value.is_a?(FalseClass) value = "false" elsif value.is_a?(TrueClass) value = "true" end value &&= @translate[value] str = opt.sub(/^--/,'') bool = true newstr = str.sub(/^no-/, '') if newstr != str str = newstr bool = false end str = str.intern if @config[str].is_a?(Puppet::Settings::BooleanSetting) if value == "" or value.nil? value = bool end end if s = @config[str] @deprecated_settings_that_have_been_configured << s if s.completely_deprecated? end @value_sets[:cli].set(str, value) unsafe_flush_cache end def include?(name) name = name.intern if name.is_a? String @config.include?(name) end # check to see if a short name is already defined def shortinclude?(short) short = short.intern if name.is_a? String @shortnames.include?(short) end # Prints the contents of a config file with the available config settings, or it # prints a single value of a config setting. def print_config_options env = value(:environment) val = value(:configprint) if val == "all" hash = {} each do |name, obj| val = value(name,env) val = val.inspect if val == "" hash[name] = val end hash.sort { |a,b| a[0].to_s <=> b[0].to_s }.each do |name, val| puts "#{name} = #{val}" end else val.split(/\s*,\s*/).sort.each do |v| if include?(v) #if there is only one value, just print it for back compatibility if v == val puts value(val,env) break end puts "#{v} = #{value(v,env)}" else puts "invalid setting: #{v}" return false end end end true end def generate_config puts to_config true end def generate_manifest puts to_manifest true end def print_configs return print_config_options if value(:configprint) != "" return generate_config if value(:genconfig) generate_manifest if value(:genmanifest) end def print_configs? (value(:configprint) != "" || value(:genconfig) || value(:genmanifest)) && true end # Return a given object's file metadata. def metadata(param) if obj = @config[param.to_sym] and obj.is_a?(FileSetting) { :owner => obj.owner, :group => obj.group, :mode => obj.mode }.delete_if { |key, value| value.nil? } else nil end end # Make a directory with the appropriate user, group, and mode def mkdir(default) obj = get_config_file_default(default) Puppet::Util::SUIDManager.asuser(obj.owner, obj.group) do mode = obj.mode || 0750 Dir.mkdir(obj.value, mode) end end # The currently configured run mode that is preferred for constructing the application configuration. def preferred_run_mode @preferred_run_mode_name || :user end # PRIVATE! This only exists because we need a hook to validate the run mode when it's being set, and # it should never, ever, ever, ever be called from outside of this file. # This method is also called when --run_mode MODE is used on the command line to set the default # # @param mode [String|Symbol] the name of the mode to have in effect # @api private def preferred_run_mode=(mode) mode = mode.to_s.downcase.intern raise ValidationError, "Invalid run mode '#{mode}'" unless [:master, :agent, :user].include?(mode) @preferred_run_mode_name = mode # Changing the run mode has far-reaching consequences. Flush any cached # settings so they will be re-generated. flush_cache mode end # Return all of the settings associated with a given section. def params(section = nil) if section section = section.intern if section.is_a? String @config.find_all { |name, obj| obj.section == section }.collect { |name, obj| name } else @config.keys end end def parse_config(text, file = "text") begin data = @config_file_parser.parse_file(file, text) rescue => detail Puppet.log_exception(detail, "Could not parse #{file}: #{detail}") return end # If we get here and don't have any data, we just return and don't muck with the current state of the world. return if data.nil? # If we get here then we have some data, so we need to clear out any # previous settings that may have come from config files. unsafe_clear(false, false) record_deprecations_from_puppet_conf(data) # And now we can repopulate with the values from our last parsing of the config files. @configuration_file = data # Determine our environment, if we have one. if @config[:environment] env = self.value(:environment).to_sym else env = "none" end # Call any hooks we should be calling. @config.values.select(&:has_hook?).each do |setting| value_sets_for(env, self.preferred_run_mode).each do |source| if source.include?(setting.name) # We still have to use value to retrieve the value, since # we want the fully interpolated value, not $vardir/lib or whatever. # This results in extra work, but so few of the settings # will have associated hooks that it ends up being less work this # way overall. if setting.call_hook_on_initialize? @hooks_to_call_on_application_initialization << setting else setting.handle(self.value(setting.name, env)) end break end end end call_hooks_deferred_to_application_initialization :ignore_interpolation_dependency_errors => true apply_metadata end # Parse the configuration file. Just provides thread safety. def parse_config_files file = which_configuration_file if Puppet::FileSystem.exist?(file) begin text = read_file(file) rescue => detail Puppet.log_exception(detail, "Could not load #{file}: #{detail}") return end else return end parse_config(text, file) end private :parse_config_files def main_config_file if explicit_config_file? return self[:config] else return File.join(Puppet::Util::RunMode[:master].conf_dir, config_file_name) end end private :main_config_file def user_config_file return File.join(Puppet::Util::RunMode[:user].conf_dir, config_file_name) end private :user_config_file # This method is here to get around some life-cycle issues. We need to be # able to determine the config file name before the settings / defaults are # fully loaded. However, we also need to respect any overrides of this value # that the user may have specified on the command line. # # The easiest way to do this is to attempt to read the setting, and if we # catch an error (meaning that it hasn't been set yet), we'll fall back to # the default value. def config_file_name begin return self[:config_file_name] if self[:config_file_name] rescue SettingsError # This just means that the setting wasn't explicitly set on the command line, so we will ignore it and # fall through to the default name. end return self.class.default_config_file_name end private :config_file_name def apply_metadata # We have to do it in the reverse of the search path, # because multiple sections could set the same value # and I'm too lazy to only set the metadata once. if @configuration_file searchpath.reverse.each do |source| source = preferred_run_mode if source == :run_mode if section = @configuration_file.sections[source] apply_metadata_from_section(section) end end end end private :apply_metadata def apply_metadata_from_section(section) section.settings.each do |setting| if setting.has_metadata? && type = @config[setting.name] type.set_meta(setting.meta) end end end SETTING_TYPES = { :string => StringSetting, :file => FileSetting, :directory => DirectorySetting, :file_or_directory => FileOrDirectorySetting, :path => PathSetting, :boolean => BooleanSetting, :terminus => TerminusSetting, :duration => DurationSetting, :ttl => TTLSetting, :array => ArraySetting, :enum => EnumSetting, :priority => PrioritySetting, :autosign => AutosignSetting, } # Create a new setting. The value is passed in because it's used to determine # what kind of setting we're creating, but the value itself might be either # a default or a value, so we can't actually assign it. # # See #define_settings for documentation on the legal values for the ":type" option. def newsetting(hash) klass = nil hash[:section] = hash[:section].to_sym if hash[:section] if type = hash[:type] unless klass = SETTING_TYPES[type] raise ArgumentError, "Invalid setting type '#{type}'" end hash.delete(:type) else # The only implicit typing we still do for settings is to fall back to "String" type if they didn't explicitly # specify a type. Personally I'd like to get rid of this too, and make the "type" option mandatory... but # there was a little resistance to taking things quite that far for now. --cprice 2012-03-19 klass = StringSetting end hash[:settings] = self setting = klass.new(hash) setting end # This has to be private, because it doesn't add the settings to @config private :newsetting # Iterate across all of the objects in a given section. def persection(section) section = section.to_sym self.each { |name, obj| if obj.section == section yield obj end } end # Reparse our config file, if necessary. def reparse_config_files if files if filename = any_files_changed? Puppet.notice "Config file #{filename} changed; triggering re-parse of all config files." parse_config_files reuse end end end def files return @files if @files @files = [] [main_config_file, user_config_file].each do |path| if Puppet::FileSystem.exist?(path) @files << Puppet::Util::WatchedFile.new(path) end end @files end private :files # Checks to see if any of the config files have been modified # @return the filename of the first file that is found to have changed, or # nil if no files have changed def any_files_changed? files.each do |file| return file.to_str if file.changed? end nil end private :any_files_changed? def reuse return unless defined?(@used) new = @used @used = [] self.use(*new) end # The order in which to search for values. def searchpath(environment = nil) [:memory, :cli, environment, :run_mode, :main, :application_defaults, :overridden_defaults].compact end # Get a list of objects per section def sectionlist sectionlist = [] self.each { |name, obj| section = obj.section || "puppet" sections[section] ||= [] sectionlist << section unless sectionlist.include?(section) sections[section] << obj } return sectionlist, sections end def service_user_available? return @service_user_available if defined?(@service_user_available) if self[:user] user = Puppet::Type.type(:user).new :name => self[:user], :audit => :ensure @service_user_available = user.exists? else @service_user_available = false end end def service_group_available? return @service_group_available if defined?(@service_group_available) if self[:group] group = Puppet::Type.type(:group).new :name => self[:group], :audit => :ensure @service_group_available = group.exists? else @service_group_available = false end end # Allow later inspection to determine if the setting was set on the # command line, or through some other code path. Used for the # `dns_alt_names` option during cert generate. --daniel 2011-10-18 def set_by_cli?(param) param = param.to_sym !@value_sets[:cli].lookup(param).nil? end def set_value(param, value, type, options = {}) Puppet.deprecation_warning("Puppet.settings.set_value is deprecated. Use Puppet[]= instead.") if @value_sets[type] @value_sets[type].set(param, value) unsafe_flush_cache end end # Deprecated; use #define_settings instead def setdefaults(section, defs) Puppet.deprecation_warning("'setdefaults' is deprecated and will be removed; please call 'define_settings' instead") define_settings(section, defs) end # Define a group of settings. # # @param [Symbol] section a symbol to use for grouping multiple settings together into a conceptual unit. This value # (and the conceptual separation) is not used very often; the main place where it will have a potential impact # is when code calls Settings#use method. See docs on that method for further details, but basically that method # just attempts to do any preparation that may be necessary before code attempts to leverage the value of a particular # setting. This has the most impact for file/directory settings, where #use will attempt to "ensure" those # files / directories. # @param [Hash[Hash]] defs the settings to be defined. This argument is a hash of hashes; each key should be a symbol, # which is basically the name of the setting that you are defining. The value should be another hash that specifies # the parameters for the particular setting. Legal values include: # [:default] => not required; this is the value for the setting if no other value is specified (via cli, config file, etc.) # For string settings this may include "variables", demarcated with $ or ${} which will be interpolated with values of other settings. # The default value may also be a Proc that will be called only once to evaluate the default when the setting's value is retrieved. # [:desc] => required; a description of the setting, used in documentation / help generation # [:type] => not required, but highly encouraged! This specifies the data type that the setting represents. If # you do not specify it, it will default to "string". Legal values include: # :string - A generic string setting # :boolean - A boolean setting; values are expected to be "true" or "false" # :file - A (single) file path; puppet may attempt to create this file depending on how the settings are used. This type # also supports additional options such as "mode", "owner", "group" # :directory - A (single) directory path; puppet may attempt to create this file depending on how the settings are used. This type # also supports additional options such as "mode", "owner", "group" # :path - This is intended to be used for settings whose value can contain multiple directory paths, respresented # as strings separated by the system path separator (e.g. system path, module path, etc.). # [:mode] => an (optional) octal value to be used as the permissions/mode for :file and :directory settings # [:owner] => optional owner username/uid for :file and :directory settings # [:group] => optional group name/gid for :file and :directory settings # def define_settings(section, defs) section = section.to_sym call = [] defs.each do |name, hash| raise ArgumentError, "setting definition for '#{name}' is not a hash!" unless hash.is_a? Hash name = name.to_sym hash[:name] = name hash[:section] = section raise ArgumentError, "Setting #{name} is already defined" if @config.include?(name) tryconfig = newsetting(hash) if short = tryconfig.short if other = @shortnames[short] raise ArgumentError, "Setting #{other.name} is already using short name '#{short}'" end @shortnames[short] = tryconfig end @config[name] = tryconfig # Collect the settings that need to have their hooks called immediately. # We have to collect them so that we can be sure we're fully initialized before # the hook is called. if tryconfig.has_hook? if tryconfig.call_hook_on_define? call << tryconfig elsif tryconfig.call_hook_on_initialize? @hooks_to_call_on_application_initialization << tryconfig end end @deprecated_setting_names << name if tryconfig.deprecated? end call.each do |setting| setting.handle(self.value(setting.name)) end end # Convert the settings we manage into a catalog full of resources that model those settings. def to_catalog(*sections) sections = nil if sections.empty? catalog = Puppet::Resource::Catalog.new("Settings", Puppet::Node::Environment::NONE) @config.keys.find_all { |key| @config[key].is_a?(FileSetting) }.each do |key| next if (key == :manifestdir && should_skip_manifestdir?()) file = @config[key] next unless (sections.nil? or sections.include?(file.section)) next unless resource = file.to_resource next if catalog.resource(resource.ref) Puppet.debug("Using settings: adding file resource '#{key}': '#{resource.inspect}'") catalog.add_resource(resource) end add_user_resources(catalog, sections) add_environment_resources(catalog, sections) catalog end def should_skip_manifestdir?() setting = @config[:environmentpath] !(setting.nil? || setting.value.nil? || setting.value.empty?) end private :should_skip_manifestdir? # Convert our list of config settings into a configuration file. def to_config str = %{The configuration file for #{Puppet.run_mode.name}. Note that this file is likely to have unused settings in it; any setting that's valid anywhere in Puppet can be in any config file, even if it's not used. Every section can specify three special parameters: owner, group, and mode. These parameters affect the required permissions of any files specified after their specification. Puppet will sometimes use these parameters to check its own configured state, so they can be used to make Puppet a bit more self-managing. The file format supports octothorpe-commented lines, but not partial-line comments. Generated on #{Time.now}. }.gsub(/^/, "# ") # Add a section heading that matches our name. str += "[#{preferred_run_mode}]\n" eachsection do |section| persection(section) do |obj| str += obj.to_config + "\n" unless obj.name == :genconfig end end return str end # Convert to a parseable manifest def to_manifest catalog = to_catalog catalog.resource_refs.collect do |ref| catalog.resource(ref).to_manifest end.join("\n\n") end # Create the necessary objects to use a section. This is idempotent; # you can 'use' a section as many times as you want. def use(*sections) sections = sections.collect { |s| s.to_sym } sections = sections.reject { |s| @used.include?(s) } return if sections.empty? begin catalog = to_catalog(*sections).to_ral rescue => detail Puppet.log_and_raise(detail, "Could not create resources for managing Puppet's files and directories in sections #{sections.inspect}: #{detail}") end catalog.host_config = false catalog.apply do |transaction| if transaction.any_failed? report = transaction.report status_failures = report.resource_statuses.values.select { |r| r.failed? } status_fail_msg = status_failures. collect(&:events). flatten. select { |event| event.status == 'failure' }. collect { |event| "#{event.resource}: #{event.message}" }.join("; ") raise "Got #{status_failures.length} failure(s) while initializing: #{status_fail_msg}" end end sections.each { |s| @used << s } @used.uniq! end def valid?(param) param = param.to_sym @config.has_key?(param) end def uninterpolated_value(param, environment = nil) Puppet.deprecation_warning("Puppet.settings.uninterpolated_value is deprecated. Use Puppet.settings.value instead") param = param.to_sym environment &&= environment.to_sym values(environment, self.preferred_run_mode).lookup(param) end # Retrieve an object that can be used for looking up values of configuration # settings. # # @param environment [Symbol] The name of the environment in which to lookup # @param section [Symbol] The name of the configuration section in which to lookup # @return [Puppet::Settings::ChainedValues] An object to perform lookups # @api public def values(environment, section) @values[environment][section] ||= ChainedValues.new( section, environment, value_sets_for(environment, section), @config) end # Find the correct value using our search path. # # @param param [String, Symbol] The value to look up # @param environment [String, Symbol] The environment to check for the value # @param bypass_interpolation [true, false] Whether to skip interpolation # # @return [Object] The looked up value # # @raise [InterpolationError] def value(param, environment = nil, bypass_interpolation = false) param = param.to_sym environment &&= environment.to_sym setting = @config[param] # Short circuit to nil for undefined settings. return nil if setting.nil? # Check the cache first. It needs to be a per-environment # cache so that we don't spread values from one env # to another. if @cache[environment||"none"].has_key?(param) return @cache[environment||"none"][param] elsif bypass_interpolation val = values(environment, self.preferred_run_mode).lookup(param) else val = values(environment, self.preferred_run_mode).interpolate(param) end @cache[environment||"none"][param] = val val end ## # (#15337) All of the logic to determine the configuration file to use # should be centralized into this method. The simplified approach is: # # 1. If there is an explicit configuration file, use that. (--confdir or # --config) # 2. If we're running as a root process, use the system puppet.conf # (usually /etc/puppet/puppet.conf) # 3. Otherwise, use the user puppet.conf (usually ~/.puppet/puppet.conf) # # @api private # @todo this code duplicates {Puppet::Util::RunMode#which_dir} as described # in {http://projects.puppetlabs.com/issues/16637 #16637} def which_configuration_file if explicit_config_file? or Puppet.features.root? then return main_config_file else return user_config_file end end # This method just turns a file into a new ConfigFile::Conf instance # @param file [String] absolute path to the configuration file # @return [Puppet::Settings::ConfigFile::Conf] # @api private def parse_file(file) @config_file_parser.parse_file(file, read_file(file)) end private DEPRECATION_REFS = { [:manifest, :modulepath, :config_version, :templatedir, :manifestdir] => "See http://links.puppetlabs.com/env-settings-deprecations" }.freeze # Record that we want to issue a deprecation warning later in the application # initialization cycle when we have settings bootstrapped to the point where # we can read the Puppet[:disable_warnings] setting. # # We are only recording warnings applicable to settings set in puppet.conf # itself. def record_deprecations_from_puppet_conf(puppet_conf) conf_sections = puppet_conf.sections.inject([]) do |accum,entry| accum << entry[1] if [:main, :master, :agent, :user].include?(entry[0]) accum end conf_sections.each do |section| section.settings.each do |conf_setting| if setting = self.setting(conf_setting.name) @deprecated_settings_that_have_been_configured << setting if setting.deprecated? end end end end def issue_deprecations @deprecated_settings_that_have_been_configured.each do |setting| issue_deprecation_warning(setting) end end def issue_deprecation_warning(setting, msg = nil) name = setting.name ref = DEPRECATION_REFS.find { |params,reference| params.include?(name) } ref = ref[1] if ref case when msg msg << " #{ref}" if ref Puppet.deprecation_warning(msg) when setting.completely_deprecated? Puppet.deprecation_warning("Setting #{name} is deprecated. #{ref}", "setting-#{name}") when setting.allowed_on_commandline? Puppet.deprecation_warning("Setting #{name} is deprecated in puppet.conf. #{ref}", "puppet-conf-setting-#{name}") end end def get_config_file_default(default) obj = nil unless obj = @config[default] raise ArgumentError, "Unknown default #{default}" end raise ArgumentError, "Default #{default} is not a file" unless obj.is_a? FileSetting obj end def add_environment_resources(catalog, sections) path = self[:environmentpath] envdir = path.split(File::PATH_SEPARATOR).first if path configured_environment = self[:environment] if configured_environment == "production" && envdir && Puppet::FileSystem.exist?(envdir) configured_environment_path = File.join(envdir, configured_environment) if !Puppet::FileSystem.symlink?(configured_environment_path) catalog.add_resource( Puppet::Resource.new(:file, configured_environment_path, :parameters => { :ensure => 'directory' }) ) end end end def add_user_resources(catalog, sections) return unless Puppet.features.root? return if Puppet.features.microsoft_windows? return unless self[:mkusers] @config.each do |name, setting| next unless setting.respond_to?(:owner) next unless sections.nil? or sections.include?(setting.section) if user = setting.owner and user != "root" and catalog.resource(:user, user).nil? resource = Puppet::Resource.new(:user, user, :parameters => {:ensure => :present}) resource[:gid] = self[:group] if self[:group] catalog.add_resource resource end if group = setting.group and ! %w{root wheel}.include?(group) and catalog.resource(:group, group).nil? catalog.add_resource Puppet::Resource.new(:group, group, :parameters => {:ensure => :present}) end end end # Yield each search source in turn. def value_sets_for(environment, mode) searchpath(environment).collect do |name| case name when :cli, :memory, :application_defaults, :overridden_defaults @value_sets[name] when :run_mode if @configuration_file section = @configuration_file.sections[mode] if section ValuesFromSection.new(mode, section) end end else values_from_section = nil if @configuration_file if section = @configuration_file.sections[name] values_from_section = ValuesFromSection.new(name, section) end end if values_from_section.nil? && global_defaults_initialized? values_from_section = ValuesFromEnvironmentConf.new(name) end values_from_section end end.compact end # Read the file in. # @api private def read_file(file) return Puppet::FileSystem.read(file) end # Private method for internal test use only; allows to do a comprehensive clear of all settings between tests. # # @return nil def clear_everything_for_tests() unsafe_clear(true, true) @configuration_file = nil @global_defaults_initialized = false @app_defaults_initialized = false end private :clear_everything_for_tests def explicit_config_file? # Figure out if the user has provided an explicit configuration file. If # so, return the path to the file, if not return nil. # # The easiest way to determine whether an explicit one has been specified # is to simply attempt to evaluate the value of ":config". This will # obviously be successful if they've passed an explicit value for :config, # but it will also result in successful interpolation if they've only # passed an explicit value for :confdir. # # If they've specified neither, then the interpolation will fail and we'll # get an exception. # begin return true if self[:config] rescue InterpolationError # This means we failed to interpolate, which means that they didn't # explicitly specify either :config or :confdir... so we'll fall out to # the default value. return false end end private :explicit_config_file? # Lookup configuration setting value through a chain of different value sources. # # @api public class ChainedValues ENVIRONMENT_SETTING = "environment".freeze ENVIRONMENT_INTERPOLATION_ALLOWED = ['config_version'].freeze # @see Puppet::Settings.values # @api private def initialize(mode, environment, value_sets, defaults) @mode = mode @environment = environment @value_sets = value_sets @defaults = defaults end # Lookup the uninterpolated value. # # @param name [Symbol] The configuration setting name to look up # @return [Object] The configuration setting value or nil if the setting is not known # @api public def lookup(name) set = @value_sets.find do |set| set.include?(name) end if set value = set.lookup(name) if !value.nil? return value end end @defaults[name].default end # Lookup the interpolated value. All instances of `$name` in the value will # be replaced by performing a lookup of `name` and substituting the text # for `$name` in the original value. This interpolation is only performed # if the looked up value is a String. # # @param name [Symbol] The configuration setting name to look up # @return [Object] The configuration setting value or nil if the setting is not known # @api public def interpolate(name) setting = @defaults[name] if setting val = lookup(name) # if we interpolate code, all hell breaks loose. if name == :code val else # Convert it if necessary begin val = convert(val, name) rescue InterpolationError => err # This happens because we don't have access to the param name when the # exception is originally raised, but we want it in the message raise InterpolationError, "Error converting value for param '#{name}': #{err}", err.backtrace end setting.munge(val) end else nil end end private def convert(value, setting_name) case value when nil nil when String failed_environment_interpolation = false interpolated_value = value.gsub(/\$(\w+)|\$\{(\w+)\}/) do |expression| varname = $2 || $1 interpolated_expression = if varname != ENVIRONMENT_SETTING || ok_to_interpolate_environment(setting_name) if varname == ENVIRONMENT_SETTING && @environment @environment elsif varname == "run_mode" @mode elsif !(pval = interpolate(varname.to_sym)).nil? pval else raise InterpolationError, "Could not find value for #{expression}" end else failed_environment_interpolation = true expression end interpolated_expression end if failed_environment_interpolation Puppet.warning("You cannot interpolate $environment within '#{setting_name}' when using directory environments. Its value will remain #{interpolated_value}.") end interpolated_value else value end end def ok_to_interpolate_environment(setting_name) return true if Puppet.settings.value(:environmentpath, nil, true).empty? ENVIRONMENT_INTERPOLATION_ALLOWED.include?(setting_name.to_s) end end class Values extend Forwardable def initialize(name, defaults) @name = name @values = {} @defaults = defaults end def_delegator :@values, :include? def_delegator :@values, :[], :lookup def set(name, value) default = @defaults[name] if !default raise ArgumentError, "Attempt to assign a value to unknown setting #{name.inspect}" end if default.has_hook? default.handle(value) end @values[name] = value end end class ValuesFromSection def initialize(name, section) @name = name @section = section end def include?(name) !@section.setting(name).nil? end def lookup(name) setting = @section.setting(name) if setting setting.value end end end # @api private class ValuesFromEnvironmentConf def initialize(environment_name) @environment_name = environment_name end def include?(name) if Puppet::Settings::EnvironmentConf::VALID_SETTINGS.include?(name) && conf return true end false end def lookup(name) return nil unless Puppet::Settings::EnvironmentConf::VALID_SETTINGS.include?(name) conf.send(name) if conf end def conf @conf ||= if environments = Puppet.lookup(:environments) environments.get_conf(@environment_name) end end end end diff --git a/lib/puppet/type/user.rb b/lib/puppet/type/user.rb index 685602399..03ef67659 100644 --- a/lib/puppet/type/user.rb +++ b/lib/puppet/type/user.rb @@ -1,688 +1,698 @@ require 'etc' require 'facter' require 'puppet/parameter/boolean' require 'puppet/property/list' require 'puppet/property/ordered_list' require 'puppet/property/keyvalue' module Puppet newtype(:user) do @doc = "Manage users. This type is mostly built to manage system users, so it is lacking some features useful for managing normal users. This resource type uses the prescribed native tools for creating groups and generally uses POSIX APIs for retrieving information about them. It does not directly modify `/etc/passwd` or anything. **Autorequires:** If Puppet is managing the user's primary group (as provided in the `gid` attribute), the user resource will autorequire that group. If Puppet is managing any role accounts corresponding to the user's roles, the user resource will autorequire those role accounts." feature :allows_duplicates, "The provider supports duplicate users with the same UID." feature :manages_homedir, "The provider can create and remove home directories." feature :manages_passwords, "The provider can modify user passwords, by accepting a password hash." feature :manages_password_age, "The provider can set age requirements and restrictions for passwords." feature :manages_password_salt, "The provider can set a password salt. This is for providers that implement PBKDF2 passwords with salt properties." feature :manages_solaris_rbac, "The provider can manage roles and normal users" feature :manages_expiry, "The provider can manage the expiry date for a user." feature :system_users, "The provider allows you to create system users with lower UIDs." feature :manages_aix_lam, "The provider can manage AIX Loadable Authentication Module (LAM) system." feature :libuser, "Allows local users to be managed on systems that also use some other remote NSS method of managing accounts." feature :manages_shell, "The provider allows for setting shell and validates if possible" newproperty(:ensure, :parent => Puppet::Property::Ensure) do newvalue(:present, :event => :user_created) do provider.create end newvalue(:absent, :event => :user_removed) do provider.delete end newvalue(:role, :event => :role_created, :required_features => :manages_solaris_rbac) do provider.create_role end desc "The basic state that the object should be in." # If they're talking about the thing at all, they generally want to # say it should exist. defaultto do if @resource.managed? :present else nil end end def retrieve if provider.exists? if provider.respond_to?(:is_role?) and provider.is_role? return :role else return :present end else return :absent end end end newproperty(:home) do desc "The home directory of the user. The directory must be created separately and is not currently checked for existence." end newproperty(:uid) do desc "The user ID; must be specified numerically. If no user ID is specified when creating a new user, then one will be chosen automatically. This will likely result in the same user having different UIDs on different systems, which is not recommended. This is especially noteworthy when managing the same user on both Darwin and other platforms, since Puppet does UID generation on Darwin, but the underlying tools do so on other platforms. On Windows, this property is read-only and will return the user's security identifier (SID)." munge do |value| case value when String if value =~ /^[-0-9]+$/ value = Integer(value) end end return value end end newproperty(:gid) do desc "The user's primary group. Can be specified numerically or by name. This attribute is not supported on Windows systems; use the `groups` attribute instead. (On Windows, designating a primary group is only meaningful for domain accounts, which Puppet does not currently manage.)" munge do |value| if value.is_a?(String) and value =~ /^[-0-9]+$/ Integer(value) else value end end def insync?(is) # We know the 'is' is a number, so we need to convert the 'should' to a number, # too. @should.each do |value| return true if number = Puppet::Util.gid(value) and is == number end false end def sync found = false @should.each do |value| if number = Puppet::Util.gid(value) provider.gid = number found = true break end end fail "Could not find group(s) #{@should.join(",")}" unless found # Use the default event. end end newproperty(:comment) do desc "A description of the user. Generally the user's full name." munge do |v| v.respond_to?(:force_encoding) ? v.force_encoding(Encoding::ASCII_8BIT) : v end end newproperty(:shell, :required_features => :manages_shell) do desc "The user's login shell. The shell must exist and be executable. This attribute cannot be managed on Windows systems." end newproperty(:password, :required_features => :manages_passwords) do desc %q{The user's password, in whatever encrypted format the local system requires. * Most modern Unix-like systems use salted SHA1 password hashes. You can use Puppet's built-in `sha1` function to generate a hash from a password. * Mac OS X 10.5 and 10.6 also use salted SHA1 hashes. * Mac OS X 10.7 (Lion) uses salted SHA512 hashes. The Puppet Labs [stdlib][] module contains a `str2saltedsha512` function which can generate password hashes for Lion. * Mac OS X 10.8 and higher use salted SHA512 PBKDF2 hashes. When managing passwords on these systems the salt and iterations properties need to be specified as well as the password. * Windows passwords can only be managed in cleartext, as there is no Windows API for setting the password hash. [stdlib]: https://github.com/puppetlabs/puppetlabs-stdlib/ Be sure to enclose any value that includes a dollar sign ($) in single quotes (') to avoid accidental variable interpolation.} validate do |value| raise ArgumentError, "Passwords cannot include ':'" if value.is_a?(String) and value.include?(":") end def change_to_s(currentvalue, newvalue) if currentvalue == :absent return "created password" else return "changed password" end end def is_to_s( currentvalue ) return '[old password hash redacted]' end def should_to_s( newvalue ) return '[new password hash redacted]' end end newproperty(:password_min_age, :required_features => :manages_password_age) do desc "The minimum number of days a password must be used before it may be changed." munge do |value| case value when String Integer(value) else value end end validate do |value| if value.to_s !~ /^-?\d+$/ raise ArgumentError, "Password minimum age must be provided as a number." end end end newproperty(:password_max_age, :required_features => :manages_password_age) do desc "The maximum number of days a password may be used before it must be changed." munge do |value| case value when String Integer(value) else value end end validate do |value| if value.to_s !~ /^-?\d+$/ raise ArgumentError, "Password maximum age must be provided as a number." end end end newproperty(:groups, :parent => Puppet::Property::List) do desc "The groups to which the user belongs. The primary group should not be listed, and groups should be identified by name rather than by GID. Multiple groups should be specified as an array." validate do |value| if value =~ /^\d+$/ raise ArgumentError, "Group names must be provided, not GID numbers." end raise ArgumentError, "Group names must be provided as an array, not a comma-separated list." if value.include?(",") raise ArgumentError, "Group names must not be empty. If you want to specify \"no groups\" pass an empty array" if value.empty? end end newparam(:name) do desc "The user name. While naming limitations vary by operating system, it is advisable to restrict names to the lowest common denominator, which is a maximum of 8 characters beginning with a letter. Note that Puppet considers user names to be case-sensitive, regardless of the platform's own rules; be sure to always use the same case when referring to a given user." isnamevar end newparam(:membership) do desc "Whether specified groups should be considered the **complete list** (`inclusive`) or the **minimum list** (`minimum`) of groups to which the user belongs. Defaults to `minimum`." newvalues(:inclusive, :minimum) defaultto :minimum end newparam(:system, :boolean => true, :parent => Puppet::Parameter::Boolean) do desc "Whether the user is a system user, according to the OS's criteria; on most platforms, a UID less than or equal to 500 indicates a system user. This parameter is only used when the resource is created and will not affect the UID when the user is present. Defaults to `false`." defaultto false end newparam(:allowdupe, :boolean => true, :parent => Puppet::Parameter::Boolean) do desc "Whether to allow duplicate UIDs. Defaults to `false`." defaultto false end newparam(:managehome, :boolean => true, :parent => Puppet::Parameter::Boolean) do desc "Whether to manage the home directory when managing the user. This will create the home directory when `ensure => present`, and delete the home directory when `ensure => absent`. Defaults to `false`." defaultto false validate do |val| if munge(val) raise ArgumentError, "User provider #{provider.class.name} can not manage home directories" if provider and not provider.class.manages_homedir? end end end newproperty(:expiry, :required_features => :manages_expiry) do desc "The expiry date for this user. Must be provided in a zero-padded YYYY-MM-DD format --- e.g. 2010-02-19. If you want to ensure the user account never expires, you can pass the special value `absent`." newvalues :absent newvalues /^\d{4}-\d{2}-\d{2}$/ validate do |value| if value.intern != :absent and value !~ /^\d{4}-\d{2}-\d{2}$/ raise ArgumentError, "Expiry dates must be YYYY-MM-DD or the string \"absent\"" end end end # Autorequire the group, if it's around autorequire(:group) do autos = [] if obj = @parameters[:gid] and groups = obj.shouldorig groups = groups.collect { |group| if group =~ /^\d+$/ Integer(group) else group end } groups.each { |group| case group when Integer if resource = catalog.resources.find { |r| r.is_a?(Puppet::Type.type(:group)) and r.should(:gid) == group } autos << resource end else autos << group end } end if obj = @parameters[:groups] and groups = obj.should autos += groups.split(",") end autos end # This method has been exposed for puppet to manage users and groups of # files in its settings and should not be considered available outside of # puppet. # # (see Puppet::Settings#service_user_available?) # # @return [Boolean] if the user exists on the system # @api private def exists? provider.exists? end def retrieve absent = false properties.inject({}) { |prophash, property| current_value = :absent if absent prophash[property] = :absent else current_value = property.retrieve prophash[property] = current_value end if property.name == :ensure and current_value == :absent absent = true end prophash } end newproperty(:roles, :parent => Puppet::Property::List, :required_features => :manages_solaris_rbac) do desc "The roles the user has. Multiple roles should be specified as an array." def membership :role_membership end validate do |value| if value =~ /^\d+$/ raise ArgumentError, "Role names must be provided, not numbers" end raise ArgumentError, "Role names must be provided as an array, not a comma-separated list" if value.include?(",") end end #autorequire the roles that the user has autorequire(:user) do reqs = [] if roles_property = @parameters[:roles] and roles = roles_property.should reqs += roles.split(',') end reqs end newparam(:role_membership) do desc "Whether specified roles should be considered the **complete list** (`inclusive`) or the **minimum list** (`minimum`) of roles the user has. Defaults to `minimum`." newvalues(:inclusive, :minimum) defaultto :minimum end newproperty(:auths, :parent => Puppet::Property::List, :required_features => :manages_solaris_rbac) do desc "The auths the user has. Multiple auths should be specified as an array." def membership :auth_membership end validate do |value| if value =~ /^\d+$/ raise ArgumentError, "Auth names must be provided, not numbers" end raise ArgumentError, "Auth names must be provided as an array, not a comma-separated list" if value.include?(",") end end newparam(:auth_membership) do desc "Whether specified auths should be considered the **complete list** (`inclusive`) or the **minimum list** (`minimum`) of auths the user has. Defaults to `minimum`." newvalues(:inclusive, :minimum) defaultto :minimum end newproperty(:profiles, :parent => Puppet::Property::OrderedList, :required_features => :manages_solaris_rbac) do desc "The profiles the user has. Multiple profiles should be specified as an array." def membership :profile_membership end validate do |value| if value =~ /^\d+$/ raise ArgumentError, "Profile names must be provided, not numbers" end raise ArgumentError, "Profile names must be provided as an array, not a comma-separated list" if value.include?(",") end end newparam(:profile_membership) do desc "Whether specified roles should be treated as the **complete list** (`inclusive`) or the **minimum list** (`minimum`) of roles of which the user is a member. Defaults to `minimum`." newvalues(:inclusive, :minimum) defaultto :minimum end newproperty(:keys, :parent => Puppet::Property::KeyValue, :required_features => :manages_solaris_rbac) do desc "Specify user attributes in an array of key = value pairs." def membership :key_membership end validate do |value| raise ArgumentError, "Key/value pairs must be separated by an =" unless value.include?("=") end end newparam(:key_membership) do desc "Whether specified key/value pairs should be considered the **complete list** (`inclusive`) or the **minimum list** (`minimum`) of the user's attributes. Defaults to `minimum`." newvalues(:inclusive, :minimum) defaultto :minimum end newproperty(:project, :required_features => :manages_solaris_rbac) do desc "The name of the project associated with a user." end newparam(:ia_load_module, :required_features => :manages_aix_lam) do desc "The name of the I&A module to use to manage this user." end newproperty(:attributes, :parent => Puppet::Property::KeyValue, :required_features => :manages_aix_lam) do desc "Specify AIX attributes for the user in an array of attribute = value pairs." def membership :attribute_membership end def delimiter " " end validate do |value| raise ArgumentError, "Attributes value pairs must be separated by an =" unless value.include?("=") end end newparam(:attribute_membership) do desc "Whether specified attribute value pairs should be treated as the **complete list** (`inclusive`) or the **minimum list** (`minimum`) of attribute/value pairs for the user. Defaults to `minimum`." newvalues(:inclusive, :minimum) defaultto :minimum end newproperty(:salt, :required_features => :manages_password_salt) do desc "This is the 32 byte salt used to generate the PBKDF2 password used in OS X. This field is required for managing passwords on OS X >= 10.8." end newproperty(:iterations, :required_features => :manages_password_salt) do desc "This is the number of iterations of a chained computation of the password hash (http://en.wikipedia.org/wiki/PBKDF2). This parameter is used in OS X. This field is required for managing passwords on OS X >= 10.8." munge do |value| if value.is_a?(String) and value =~/^[-0-9]+$/ Integer(value) else value end end end newparam(:forcelocal, :boolean => true, :required_features => :libuser, :parent => Puppet::Parameter::Boolean) do desc "Forces the management of local accounts when accounts are also being managed by some other NSS" defaultto false end def generate return [] if self[:purge_ssh_keys].empty? find_unmanaged_keys end newparam(:purge_ssh_keys) do desc "Whether to purge authorized SSH keys for this user if they are not managed with the `ssh_authorized_key` resource type. Allowed values are: * `false` (default) --- don't purge SSH keys for this user. * `true` --- look for keys in the `.ssh/authorized_keys` file in the user's home directory. Purge any keys that aren't managed as `ssh_authorized_key` resources. * An array of file paths --- look for keys in all of the files listed. Purge any keys that aren't managed as `ssh_authorized_key` resources. If any of these paths starts with `~` or `%h`, that token will be replaced with the user's home directory." defaultto :false # Use Symbols instead of booleans until PUP-1967 is resolved. newvalues(:true, :false) validate do |value| if [ :true, :false ].include? value.to_s.intern return end value = [ value ] if value.is_a?(String) if value.is_a?(Array) value.each do |entry| raise ArgumentError, "Each entry for purge_ssh_keys must be a string, not a #{entry.class}" unless entry.is_a?(String) valid_home = Puppet::Util.absolute_path?(entry) || entry =~ %r{^~/|^%h/} raise ArgumentError, "Paths to keyfiles must be absolute, not #{entry}" unless valid_home end return end raise ArgumentError, "purge_ssh_keys must be true, false, or an array of file names, not #{value.inspect}" end munge do |value| # Resolve string, boolean and symbol forms of true and false to a # single representation. test_sym = value.to_s.intern value = test_sym if [:true, :false].include? test_sym return [] if value == :false home = resource[:home] if value == :true and not home raise ArgumentError, "purge_ssh_keys can only be true for users with a defined home directory" end return [ "#{home}/.ssh/authorized_keys" ] if value == :true # value is an array - munge each value [ value ].flatten.map do |entry| if entry =~ /^~|^%h/ and not home raise ArgumentError, "purge_ssh_keys value '#{value}' meta character ~ or %h only allowed for users with a defined home directory" end entry.gsub!(/^~\//, "#{home}/") entry.gsub!(/^%h\//, "#{home}/") entry end end end # Generate ssh_authorized_keys resources for purging. The key files are # taken from the purge_ssh_keys parameter. The generated resources inherit # all metaparameters from the parent user resource. # # @return [Array keyname, :target => keyfile) }.reject { |res| catalog.resource_refs.include? res.ref } end end end diff --git a/spec/fixtures/unit/type/user/authorized_keys b/spec/fixtures/unit/type/user/authorized_keys index dd1807e56..d58c62008 100644 --- a/spec/fixtures/unit/type/user/authorized_keys +++ b/spec/fixtures/unit/type/user/authorized_keys @@ -1,5 +1,6 @@ # fixture for testing ssh key purging ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDTXvM7AslzjNUYrPLiNVBsF5VnqL2RmqrkzscdVdHzVxvieNwmLGeUkg8EfXPiz7j5F/Lr0J8oItTCWzyN2KmM+DhUMjvP4AbELO/VYbnVrZICRiUNYSO3EN9/uapKAuiev88d7ynbonCU0VZoTPg/ug4OondOrLCtcGri5ltF+mausGfAYiFAQVEWqXV+1tyejoawJ884etb3n4ilpsrH9JK6AtOkEWVD3TDrNi29O1mQQ/Cn88g472zAJ+DhsIn+iehtfX5nmOtDNN/1t1bGMIBzkSYEAYwUiRJbRXvbobT7qKZQPA3dh0m8AYQS5/hd4/c4pmlxL8kgr24SnBY5 key1 name ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDTXvM7AslzjNUYrPLiNVBsF5VnqL2RmqrkzscdVdHzVxvieNwmLGeUkg8EfXPiz7j5F/Lr0J8oItTCWzyN2KmM+DhUMjvP4AbELO/VYbnVrZICRiUNYSO3EN9/uapKAuiev88d7ynbonCU0VZoTPg/ug4OondOrLCtcGri5ltF+mausGfAYiFAQVEWqXV+1tyejoawJ884etb3n4ilpsrH9JK6AtOkEWVD3TDrNi29O1mQQ/Cn88g472zAJ+DhsIn+iehtfX5nmOtDNN/1t1bGMIBzkSYEAYwUiRJbRXvbobT7qKZQPA3dh0m8AYQS5/hd4/c4pmlxL8kgr24SnBY5 keyname2 #ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDTXvM7AslzjNUYrPLiNVBsF5VnqL2RmqrkzscdVdHzVxvieNwmLGeUkg8EfXPiz7j5F/Lr0J8oItTCWzyN2KmM+DhUMjvP4AbELO/VYbnVrZICRiUNYSO3EN9/uapKAuiev88d7ynbonCU0VZoTPg/ug4OondOrLCtcGri5ltF+mausGfAYiFAQVEWqXV+1tyejoawJ884etb3n4ilpsrH9JK6AtOkEWVD3TDrNi29O1mQQ/Cn88g472zAJ+DhsIn+iehtfX5nmOtDNN/1t1bGMIBzkSYEAYwUiRJbRXvbobT7qKZQPA3dh0m8AYQS5/hd4/c4pmlxL8kgr24SnBY5 keyname3 +ssh-rsa KEY-WITH-NO-NAME diff --git a/spec/integration/parser/future_compiler_spec.rb b/spec/integration/parser/future_compiler_spec.rb index d0fcfcdec..9b612e400 100644 --- a/spec/integration/parser/future_compiler_spec.rb +++ b/spec/integration/parser/future_compiler_spec.rb @@ -1,750 +1,806 @@ require 'spec_helper' require 'puppet/pops' require 'puppet/parser/parser_factory' require 'puppet_spec/compiler' require 'puppet_spec/pops' require 'puppet_spec/scope' require 'matchers/resource' require 'rgen/metamodel_builder' # Test compilation using the future evaluator describe "Puppet::Parser::Compiler" do include PuppetSpec::Compiler include Matchers::Resource before :each do Puppet[:parser] = 'future' end describe "the compiler when using future parser and evaluator" do it "should be able to determine the configuration version from a local version control repository" do pending("Bug #14071 about semantics of Puppet::Util::Execute on Windows", :if => Puppet.features.microsoft_windows?) do # This should always work, because we should always be # in the puppet repo when we run this. version = %x{git rev-parse HEAD}.chomp Puppet.settings[:config_version] = 'git rev-parse HEAD' compiler = Puppet::Parser::Compiler.new(Puppet::Node.new("testnode")) compiler.catalog.version.should == version end end it "should not create duplicate resources when a class is referenced both directly and indirectly by the node classifier (4792)" do node = Puppet::Node.new("testnodex") node.classes = ['foo', 'bar'] catalog = compile_to_catalog(<<-PP, node) class foo { notify { foo_notify: } include bar } class bar { notify { bar_notify: } } PP catalog = Puppet::Parser::Compiler.compile(node) expect(catalog).to have_resource("Notify[foo_notify]") expect(catalog).to have_resource("Notify[bar_notify]") end it 'applies defaults for defines with qualified names (PUP-2302)' do catalog = compile_to_catalog(<<-CODE) define my::thing($msg = 'foo') { notify {'check_me': message => $msg } } My::Thing { msg => 'evoe' } my::thing { 'name': } CODE expect(catalog).to have_resource("Notify[check_me]").with_parameter(:message, "evoe") end it 'Applies defaults from dynamic scopes (3x and future with reverted PUP-867)' do catalog = compile_to_catalog(<<-CODE) class a { Notify { message => "defaulted" } include b notify { bye: } } class b { notify { hi: } } include a CODE expect(catalog).to have_resource("Notify[hi]").with_parameter(:message, "defaulted") expect(catalog).to have_resource("Notify[bye]").with_parameter(:message, "defaulted") end it 'gets default from inherited class (PUP-867)' do catalog = compile_to_catalog(<<-CODE) class a { Notify { message => "defaulted" } include c notify { bye: } } class b { Notify { message => "inherited" } } class c inherits b { notify { hi: } } include a CODE expect(catalog).to have_resource("Notify[hi]").with_parameter(:message, "inherited") expect(catalog).to have_resource("Notify[bye]").with_parameter(:message, "defaulted") end it 'looks up default parameter values from inherited class (PUP-2532)' do catalog = compile_to_catalog(<<-CODE) class a { Notify { message => "defaulted" } include c notify { bye: } } class b { Notify { message => "inherited" } } class c inherits b { notify { hi: } } include a notify {hi_test: message => Notify[hi][message] } notify {bye_test: message => Notify[bye][message] } CODE expect(catalog).to have_resource("Notify[hi_test]").with_parameter(:message, "inherited") expect(catalog).to have_resource("Notify[bye_test]").with_parameter(:message, "defaulted") end it 'does not allow override of class parameters using a resource override expression' do expect do compile_to_catalog(<<-CODE) Class[a] { x => 2} CODE end.to raise_error(/Resource Override can only.*got: Class\[a\].*/) end describe "when resolving class references" do it "should not favor local scope (with class included in topscope)" do catalog = compile_to_catalog(<<-PP) class experiment { class baz { } notify {"x" : require => Class[Baz] } notify {"y" : require => Class[Experiment::Baz] } } class baz { } include baz include experiment include experiment::baz PP expect(catalog).to have_resource("Notify[x]").with_parameter(:require, be_resource("Class[Baz]")) expect(catalog).to have_resource("Notify[y]").with_parameter(:require, be_resource("Class[Experiment::Baz]")) end it "should not favor local scope, (with class not included in topscope)" do catalog = compile_to_catalog(<<-PP) class experiment { class baz { } notify {"x" : require => Class[Baz] } notify {"y" : require => Class[Experiment::Baz] } } class baz { } include experiment include experiment::baz PP expect(catalog).to have_resource("Notify[x]").with_parameter(:require, be_resource("Class[Baz]")) expect(catalog).to have_resource("Notify[y]").with_parameter(:require, be_resource("Class[Experiment::Baz]")) end end describe "(ticket #13349) when explicitly specifying top scope" do ["class {'::bar::baz':}", "include ::bar::baz"].each do |include| describe "with #{include}" do it "should find the top level class" do catalog = compile_to_catalog(<<-MANIFEST) class { 'foo::test': } class foo::test { #{include} } class bar::baz { notify { 'good!': } } class foo::bar::baz { notify { 'bad!': } } MANIFEST expect(catalog).to have_resource("Class[Bar::Baz]") expect(catalog).to have_resource("Notify[good!]") expect(catalog).to_not have_resource("Class[Foo::Bar::Baz]") expect(catalog).to_not have_resource("Notify[bad!]") end end end end it "should recompute the version after input files are re-parsed" do Puppet[:code] = 'class foo { }' Time.stubs(:now).returns(1) node = Puppet::Node.new('mynode') Puppet::Parser::Compiler.compile(node).version.should == 1 Time.stubs(:now).returns(2) Puppet::Parser::Compiler.compile(node).version.should == 1 # no change because files didn't change Puppet::Resource::TypeCollection.any_instance.stubs(:stale?).returns(true).then.returns(false) # pretend change Puppet::Parser::Compiler.compile(node).version.should == 2 end ['define', 'class', 'node'].each do |thing| it "'#{thing}' is not allowed inside evaluated conditional constructs" do expect do compile_to_catalog(<<-PP) if true { #{thing} foo { } notify { decoy: } } PP end.to raise_error(Puppet::Error, /Classes, definitions, and nodes may only appear at toplevel/) end it "'#{thing}' is not allowed inside un-evaluated conditional constructs" do expect do compile_to_catalog(<<-PP) if false { #{thing} foo { } notify { decoy: } } PP end.to raise_error(Puppet::Error, /Classes, definitions, and nodes may only appear at toplevel/) end end describe "relationships can be formed" do def extract_name(ref) ref.sub(/File\[(\w+)\]/, '\1') end def assert_creates_relationships(relationship_code, expectations) base_manifest = <<-MANIFEST file { [a,b,c]: mode => '0644', } file { [d,e]: mode => '0755', } MANIFEST catalog = compile_to_catalog(base_manifest + relationship_code) resources = catalog.resources.select { |res| res.type == 'File' } actual_relationships, actual_subscriptions = [:before, :notify].map do |relation| resources.map do |res| dependents = Array(res[relation]) dependents.map { |ref| [res.title, extract_name(ref)] } end.inject(&:concat) end actual_relationships.should =~ (expectations[:relationships] || []) actual_subscriptions.should =~ (expectations[:subscriptions] || []) end it "of regular type" do assert_creates_relationships("File[a] -> File[b]", :relationships => [['a','b']]) end it "of subscription type" do assert_creates_relationships("File[a] ~> File[b]", :subscriptions => [['a', 'b']]) end it "between multiple resources expressed as resource with multiple titles" do assert_creates_relationships("File[a,b] -> File[c,d]", :relationships => [['a', 'c'], ['b', 'c'], ['a', 'd'], ['b', 'd']]) end it "between collection expressions" do assert_creates_relationships("File <| mode == 0644 |> -> File <| mode == 0755 |>", :relationships => [['a', 'd'], ['b', 'd'], ['c', 'd'], ['a', 'e'], ['b', 'e'], ['c', 'e']]) end it "between resources expressed as Strings" do assert_creates_relationships("'File[a]' -> 'File[b]'", :relationships => [['a', 'b']]) end it "between resources expressed as variables" do assert_creates_relationships(<<-MANIFEST, :relationships => [['a', 'b']]) $var = File[a] $var -> File[b] MANIFEST end it "between resources expressed as case statements" do assert_creates_relationships(<<-MANIFEST, :relationships => [['s1', 't2']]) $var = 10 case $var { 10: { file { s1: } } 12: { file { s2: } } } -> case $var + 2 { 10: { file { t1: } } 12: { file { t2: } } } MANIFEST end it "using deep access in array" do assert_creates_relationships(<<-MANIFEST, :relationships => [['a', 'b']]) $var = [ [ [ File[a], File[b] ] ] ] $var[0][0][0] -> $var[0][0][1] MANIFEST end it "using deep access in hash" do assert_creates_relationships(<<-MANIFEST, :relationships => [['a', 'b']]) $var = {'foo' => {'bar' => {'source' => File[a], 'target' => File[b]}}} $var[foo][bar][source] -> $var[foo][bar][target] MANIFEST end it "using resource declarations" do assert_creates_relationships("file { l: } -> file { r: }", :relationships => [['l', 'r']]) end it "between entries in a chain of relationships" do assert_creates_relationships("File[a] -> File[b] ~> File[c] <- File[d] <~ File[e]", :relationships => [['a', 'b'], ['d', 'c']], :subscriptions => [['b', 'c'], ['e', 'd']]) end end context "when dealing with variable references" do it 'an initial underscore in a variable name is ok' do catalog = compile_to_catalog(<<-MANIFEST) class a { $_a = 10} include a notify { 'test': message => $a::_a } MANIFEST expect(catalog).to have_resource("Notify[test]").with_parameter(:message, 10) end it 'an initial underscore in not ok if elsewhere than last segment' do expect do catalog = compile_to_catalog(<<-MANIFEST) class a { $_a = 10} include a notify { 'test': message => $_a::_a } MANIFEST end.to raise_error(/Illegal variable name/) end it 'a missing variable as default value becomes undef' do # strict variables not on catalog = compile_to_catalog(<<-MANIFEST) class a ($b=$x) { notify {test: message=>"yes ${undef == $b}" } } include a MANIFEST expect(catalog).to have_resource("Notify[test]").with_parameter(:message, "yes true") end end context 'when working with the trusted data hash' do context 'and have opted in to hashed_node_data' do before :each do Puppet[:trusted_node_data] = true end it 'should make $trusted available' do node = Puppet::Node.new("testing") node.trusted_data = { "data" => "value" } catalog = compile_to_catalog(<<-MANIFEST, node) notify { 'test': message => $trusted[data] } MANIFEST expect(catalog).to have_resource("Notify[test]").with_parameter(:message, "value") end it 'should not allow assignment to $trusted' do node = Puppet::Node.new("testing") node.trusted_data = { "data" => "value" } expect do compile_to_catalog(<<-MANIFEST, node) $trusted = 'changed' notify { 'test': message => $trusted == 'changed' } MANIFEST end.to raise_error(Puppet::Error, /Attempt to assign to a reserved variable name: 'trusted'/) end end context 'and have not opted in to hashed_node_data' do before :each do Puppet[:trusted_node_data] = false end it 'should not make $trusted available' do node = Puppet::Node.new("testing") node.trusted_data = { "data" => "value" } catalog = compile_to_catalog(<<-MANIFEST, node) notify { 'test': message => ($trusted == undef) } MANIFEST expect(catalog).to have_resource("Notify[test]").with_parameter(:message, true) end it 'should allow assignment to $trusted' do catalog = compile_to_catalog(<<-MANIFEST) $trusted = 'changed' notify { 'test': message => $trusted == 'changed' } MANIFEST expect(catalog).to have_resource("Notify[test]").with_parameter(:message, true) end end end context 'when using typed parameters in definition' do it 'accepts type compliant arguments' do catalog = compile_to_catalog(<<-MANIFEST) define foo(String $x) { } foo { 'test': x =>'say friend' } MANIFEST expect(catalog).to have_resource("Foo[test]").with_parameter(:x, 'say friend') end + it 'accepts undef as the default for an Optional argument' do + catalog = compile_to_catalog(<<-MANIFEST) + define foo(Optional[String] $x = undef) { + notify { "expected": message => $x == undef } + } + foo { 'test': } + MANIFEST + expect(catalog).to have_resource("Notify[expected]").with_parameter(:message, true) + end + it 'accepts anything when parameters are untyped' do expect do catalog = compile_to_catalog(<<-MANIFEST) define foo($a, $b, $c) { } foo { 'test': a => String, b=>10, c=>undef } MANIFEST end.to_not raise_error() end it 'denies non type compliant arguments' do expect do catalog = compile_to_catalog(<<-MANIFEST) define foo(Integer $x) { } foo { 'test': x =>'say friend' } MANIFEST end.to raise_error(/type Integer, got String/) end + it 'denies undef for a non-optional type' do + expect do + catalog = compile_to_catalog(<<-MANIFEST) + define foo(Integer $x) { } + foo { 'test': x => undef } + MANIFEST + end.to raise_error(/type Integer, got Undef/) + end + it 'denies non type compliant default argument' do expect do catalog = compile_to_catalog(<<-MANIFEST) define foo(Integer $x = 'pow') { } foo { 'test': } MANIFEST end.to raise_error(/type Integer, got String/) end + it 'denies undef as the default for a non-optional type' do + expect do + catalog = compile_to_catalog(<<-MANIFEST) + define foo(Integer $x = undef) { } + foo { 'test': } + MANIFEST + end.to raise_error(/type Integer, got Undef/) + end + it 'accepts a Resource as a Type' do catalog = compile_to_catalog(<<-MANIFEST) define foo(Type[Bar] $x) { notify { 'test': message => $x[text] } } define bar($text) { } bar { 'joke': text => 'knock knock' } foo { 'test': x => Bar[joke] } MANIFEST expect(catalog).to have_resource("Notify[test]").with_parameter(:message, 'knock knock') end end context 'when using typed parameters in class' do it 'accepts type compliant arguments' do catalog = compile_to_catalog(<<-MANIFEST) class foo(String $x) { } class { 'foo': x =>'say friend' } MANIFEST expect(catalog).to have_resource("Class[Foo]").with_parameter(:x, 'say friend') end + it 'accepts undef as the default for an Optional argument' do + catalog = compile_to_catalog(<<-MANIFEST) + class foo(Optional[String] $x = undef) { + notify { "expected": message => $x == undef } + } + class { 'foo': } + MANIFEST + expect(catalog).to have_resource("Notify[expected]").with_parameter(:message, true) + end + it 'accepts anything when parameters are untyped' do expect do catalog = compile_to_catalog(<<-MANIFEST) class foo($a, $b, $c) { } class { 'foo': a => String, b=>10, c=>undef } MANIFEST end.to_not raise_error() end it 'denies non type compliant arguments' do expect do catalog = compile_to_catalog(<<-MANIFEST) class foo(Integer $x) { } class { 'foo': x =>'say friend' } MANIFEST end.to raise_error(/type Integer, got String/) end + it 'denies undef for a non-optional type' do + expect do + catalog = compile_to_catalog(<<-MANIFEST) + class foo(Integer $x) { } + class { 'foo': x => undef } + MANIFEST + end.to raise_error(/type Integer, got Undef/) + end + it 'denies non type compliant default argument' do expect do catalog = compile_to_catalog(<<-MANIFEST) class foo(Integer $x = 'pow') { } class { 'foo': } MANIFEST end.to raise_error(/type Integer, got String/) end + it 'denies undef as the default for a non-optional type' do + expect do + catalog = compile_to_catalog(<<-MANIFEST) + class foo(Integer $x = undef) { } + class { 'foo': } + MANIFEST + end.to raise_error(/type Integer, got Undef/) + end + it 'accepts a Resource as a Type' do catalog = compile_to_catalog(<<-MANIFEST) class foo(Type[Bar] $x) { notify { 'test': message => $x[text] } } define bar($text) { } bar { 'joke': text => 'knock knock' } class { 'foo': x => Bar[joke] } MANIFEST expect(catalog).to have_resource("Notify[test]").with_parameter(:message, 'knock knock') end end context 'when using typed parameters in lambdas' do it 'accepts type compliant arguments' do catalog = compile_to_catalog(<<-MANIFEST) with('value') |String $x| { notify { "$x": } } MANIFEST expect(catalog).to have_resource("Notify[value]") end it 'handles an array as a single argument' do catalog = compile_to_catalog(<<-MANIFEST) with(['value', 'second']) |$x| { notify { "${x[0]} ${x[1]}": } } MANIFEST expect(catalog).to have_resource("Notify[value second]") end it 'denies when missing required arguments' do expect do compile_to_catalog(<<-MANIFEST) with(1) |$x, $y| { } MANIFEST end.to raise_error(/Parameter \$y is required but no value was given/m) end it 'accepts anything when parameters are untyped' do catalog = compile_to_catalog(<<-MANIFEST) ['value', 1, true, undef].each |$x| { notify { "value: $x": } } MANIFEST expect(catalog).to have_resource("Notify[value: value]") expect(catalog).to have_resource("Notify[value: 1]") expect(catalog).to have_resource("Notify[value: true]") expect(catalog).to have_resource("Notify[value: ]") end it 'accepts type-compliant, slurped arguments' do catalog = compile_to_catalog(<<-MANIFEST) with(1, 2) |Integer *$x| { notify { "${$x[0] + $x[1]}": } } MANIFEST expect(catalog).to have_resource("Notify[3]") end it 'denies non-type-compliant arguments' do expect do compile_to_catalog(<<-MANIFEST) with(1) |String $x| { } MANIFEST end.to raise_error(/expected.*String.*actual.*Integer/m) end it 'denies non-type-compliant, slurped arguments' do expect do compile_to_catalog(<<-MANIFEST) with(1, "hello") |Integer *$x| { } MANIFEST end.to raise_error(/called with mis-matched arguments.*expected.*Integer.*actual.*Integer, String/m) end it 'denies non-type-compliant default argument' do expect do compile_to_catalog(<<-MANIFEST) with(1) |$x, String $defaulted = 1| { notify { "${$x + $defaulted}": }} MANIFEST end.to raise_error(/expected.*Any.*String.*actual.*Integer.*Integer/m) end it 'raises an error when a default argument value is an incorrect type and there are no arguments passed' do expect do compile_to_catalog(<<-MANIFEST) with() |String $defaulted = 1| {} MANIFEST end.to raise_error(/expected.*String.*actual.*Integer/m) end it 'raises an error when the default argument for a slurped parameter is an incorrect type' do expect do compile_to_catalog(<<-MANIFEST) with() |String *$defaulted = 1| {} MANIFEST end.to raise_error(/expected.*String.*actual.*Integer/m) end it 'allows using an array as the default slurped value' do catalog = compile_to_catalog(<<-MANIFEST) with() |String *$defaulted = [hi]| { notify { $defaulted[0]: } } MANIFEST expect(catalog).to have_resource('Notify[hi]') end it 'allows using a value of the type as the default slurped value' do catalog = compile_to_catalog(<<-MANIFEST) with() |String *$defaulted = hi| { notify { $defaulted[0]: } } MANIFEST expect(catalog).to have_resource('Notify[hi]') end it 'allows specifying the type of a slurped parameter as an array' do catalog = compile_to_catalog(<<-MANIFEST) with() |Array[String] *$defaulted = hi| { notify { $defaulted[0]: } } MANIFEST expect(catalog).to have_resource('Notify[hi]') end it 'raises an error when the number of default values does not match the parameter\'s size specification' do expect do compile_to_catalog(<<-MANIFEST) with() |Array[String, 2] *$defaulted = hi| { } MANIFEST end.to raise_error(/expected.*arg count \{2,\}.*actual.*arg count \{1\}/m) end it 'raises an error when the number of passed values does not match the parameter\'s size specification' do expect do compile_to_catalog(<<-MANIFEST) with(hi) |Array[String, 2] *$passed| { } MANIFEST end.to raise_error(/expected.*arg count \{2,\}.*actual.*arg count \{1\}/m) end it 'matches when the number of arguments passed for a slurp parameter match the size specification' do catalog = compile_to_catalog(<<-MANIFEST) with(hi, bye) |Array[String, 2] *$passed| { $passed.each |$n| { notify { $n: } } } MANIFEST expect(catalog).to have_resource('Notify[hi]') expect(catalog).to have_resource('Notify[bye]') end it 'raises an error when the number of allowed slurp parameters exceeds the size constraint' do expect do compile_to_catalog(<<-MANIFEST) with(hi, bye) |Array[String, 1, 1] *$passed| { } MANIFEST end.to raise_error(/expected.*arg count \{1\}.*actual.*arg count \{2\}/m) end it 'allows passing slurped arrays by specifying an array of arrays' do catalog = compile_to_catalog(<<-MANIFEST) with([hi], [bye]) |Array[Array[String, 1, 1]] *$passed| { notify { $passed[0][0]: } notify { $passed[1][0]: } } MANIFEST expect(catalog).to have_resource('Notify[hi]') expect(catalog).to have_resource('Notify[bye]') end it 'raises an error when a required argument follows an optional one' do expect do compile_to_catalog(<<-MANIFEST) with() |$y = first, $x, Array[String, 1] *$passed = bye| {} MANIFEST end.to raise_error(/Parameter \$x is required/) end it 'raises an error when the minimum size of a slurped argument makes it required and it follows an optional argument' do expect do compile_to_catalog(<<-MANIFEST) with() |$x = first, Array[String, 1] *$passed| {} MANIFEST end.to raise_error(/Parameter \$passed is required/) end it 'allows slurped arguments with a minimum size of 0 after an optional argument' do catalog = compile_to_catalog(<<-MANIFEST) with() |$x = first, Array[String, 0] *$passed| { notify { $x: } } MANIFEST expect(catalog).to have_resource('Notify[first]') end it 'accepts a Resource as a Type' do catalog = compile_to_catalog(<<-MANIFEST) define bar($text) { } bar { 'joke': text => 'knock knock' } with(Bar[joke]) |Type[Bar] $joke| { notify { "${joke[text]}": } } MANIFEST expect(catalog).to have_resource("Notify[knock knock]") end end end context 'when evaluating collection' do it 'matches on container inherited tags' do Puppet[:code] = <<-MANIFEST class xport_test { tag('foo_bar') @notify { 'nbr1': message => 'explicitly tagged', tag => 'foo_bar' } @notify { 'nbr2': message => 'implicitly tagged' } Notify <| tag == 'foo_bar' |> { message => 'overridden' } } include xport_test MANIFEST catalog = Puppet::Parser::Compiler.compile(Puppet::Node.new("mynode")) expect(catalog).to have_resource("Notify[nbr1]").with_parameter(:message, 'overridden') expect(catalog).to have_resource("Notify[nbr2]").with_parameter(:message, 'overridden') end end end diff --git a/spec/integration/type/user_spec.rb b/spec/integration/type/user_spec.rb index 4724fe9d5..c542e51a9 100644 --- a/spec/integration/type/user_spec.rb +++ b/spec/integration/type/user_spec.rb @@ -1,36 +1,57 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet_spec/files' require 'puppet_spec/compiler' describe Puppet::Type.type(:user), '(integration)', :unless => Puppet.features.microsoft_windows? do include PuppetSpec::Files include PuppetSpec::Compiler context "when set to purge ssh keys from a file" do - let(:tempfile) { file_containing('user_spec', "# comment\nssh-rsa KEY-DATA key-name\nssh-rsa KEY-DATA key name\n") } + let(:tempfile) do + file_containing('user_spec', <<-EOF) + # comment + ssh-rsa KEY-DATA key-name + ssh-rsa KEY-DATA key name + EOF + end # must use an existing user, or the generated key resource # will fail on account of an invalid user for the key # - root should be a safe default let(:manifest) { "user { 'root': purge_ssh_keys => '#{tempfile}' }" } it "should purge authorized ssh keys" do apply_compiled_manifest(manifest) File.read(tempfile).should_not =~ /key-name/ end it "should purge keys with spaces in the comment string" do apply_compiled_manifest(manifest) File.read(tempfile).should_not =~ /key name/ end context "with other prefetching resources evaluated first" do let(:manifest) { "host { 'test': before => User[root] } user { 'root': purge_ssh_keys => '#{tempfile}' }" } it "should purge authorized ssh keys" do apply_compiled_manifest(manifest) File.read(tempfile).should_not =~ /key-name/ end end + + context "with multiple unnamed keys" do + let(:tempfile) do + file_containing('user_spec', <<-EOF) + # comment + ssh-rsa KEY-DATA1 + ssh-rsa KEY-DATA2 + EOF + end + + it "should purge authorized ssh keys" do + apply_compiled_manifest(manifest) + File.read(tempfile).should_not =~ /KEY-DATA/ + end + end end end diff --git a/spec/unit/environments_spec.rb b/spec/unit/environments_spec.rb index e68abd5ce..fe0814814 100644 --- a/spec/unit/environments_spec.rb +++ b/spec/unit/environments_spec.rb @@ -1,433 +1,482 @@ require 'spec_helper' require 'puppet/environments' require 'puppet/file_system' require 'matchers/include' module PuppetEnvironments describe Puppet::Environments do include Matchers::Include FS = Puppet::FileSystem describe "directories loader" do before(:each) do Puppet.settings.initialize_global_settings end it "lists environments" do global_path_1_location = File.expand_path("global_path_1") global_path_2_location = File.expand_path("global_path_2") global_path_1 = FS::MemoryFile.a_directory(global_path_1_location) global_path_2 = FS::MemoryFile.a_directory(global_path_2_location) envdir = FS::MemoryFile.a_directory(File.expand_path("envdir"), [ FS::MemoryFile.a_directory("env1", [ FS::MemoryFile.a_missing_file("environment.conf"), FS::MemoryFile.a_directory("modules"), FS::MemoryFile.a_directory("manifests"), ]), FS::MemoryFile.a_directory("env2", [ FS::MemoryFile.a_missing_file("environment.conf"), ]), ]) loader_from(:filesystem => [envdir, global_path_1, global_path_2], :directory => envdir, :modulepath => [global_path_1_location, global_path_2_location]) do |loader| expect(loader.list).to include_in_any_order( environment(:env1). with_manifest("#{FS.path_string(envdir)}/env1/manifests"). with_modulepath(["#{FS.path_string(envdir)}/env1/modules", global_path_1_location, global_path_2_location]), environment(:env2)) end end it "does not list files" do envdir = FS::MemoryFile.a_directory(File.expand_path("envdir"), [ FS::MemoryFile.a_regular_file_containing("foo", ''), FS::MemoryFile.a_directory("env1", [ FS::MemoryFile.a_missing_file("environment.conf"), ]), FS::MemoryFile.a_directory("env2", [ FS::MemoryFile.a_missing_file("environment.conf"), ]), ]) loader_from(:filesystem => [envdir], :directory => envdir) do |loader| expect(loader.list).to include_in_any_order(environment(:env1), environment(:env2)) end end it "ignores directories that are not valid env names (alphanumeric and _)" do envdir = FS::MemoryFile.a_directory(File.expand_path("envdir"), [ FS::MemoryFile.a_directory(".foo"), FS::MemoryFile.a_directory("bar-thing"), FS::MemoryFile.a_directory("with spaces"), FS::MemoryFile.a_directory("some.thing"), FS::MemoryFile.a_directory("env1", [ FS::MemoryFile.a_missing_file("environment.conf"), ]), FS::MemoryFile.a_directory("env2", [ FS::MemoryFile.a_missing_file("environment.conf"), ]), ]) loader_from(:filesystem => [envdir], :directory => envdir) do |loader| expect(loader.list).to include_in_any_order(environment(:env1), environment(:env2)) end end it "gets a particular environment" do directory_tree = FS::MemoryFile.a_directory(File.expand_path("envdir"), [ FS::MemoryFile.a_directory("env1", [ FS::MemoryFile.a_missing_file("environment.conf"), ]), FS::MemoryFile.a_directory("env2", [ FS::MemoryFile.a_missing_file("environment.conf"), ]), ]) loader_from(:filesystem => [directory_tree], :directory => directory_tree) do |loader| expect(loader.get("env1")).to environment(:env1) end end it "raises error when environment not found" do directory_tree = FS::MemoryFile.a_directory(File.expand_path("envdir"), []) loader_from(:filesystem => [directory_tree], :directory => directory_tree) do |loader| expect do loader.get!("does_not_exist") end.to raise_error(Puppet::Environments::EnvironmentNotFound) end end it "returns nil if an environment can't be found" do directory_tree = FS::MemoryFile.a_directory("envdir", []) loader_from(:filesystem => [directory_tree], :directory => directory_tree) do |loader| expect(loader.get("env_not_in_this_list")).to be_nil end end it "raises error if an environment can't be found" do directory_tree = FS::MemoryFile.a_directory("envdir", []) loader_from(:filesystem => [directory_tree], :directory => directory_tree) do |loader| expect do loader.get!("env_not_in_this_list") end.to raise_error(Puppet::Environments::EnvironmentNotFound) end end context "with an environment.conf" do let(:envdir) do FS::MemoryFile.a_directory(File.expand_path("envdir"), [ FS::MemoryFile.a_directory("env1", [ FS::MemoryFile.a_regular_file_containing("environment.conf", content), ]), ]) end let(:manifestdir) { FS::MemoryFile.a_directory(File.expand_path("/some/manifest/path")) } let(:modulepath) do [ FS::MemoryFile.a_directory(File.expand_path("/some/module/path")), FS::MemoryFile.a_directory(File.expand_path("/some/other/path")), ] end let(:content) do <<-EOF manifest=#{manifestdir} modulepath=#{modulepath.join(File::PATH_SEPARATOR)} config_version=/some/script EOF end it "reads environment.conf settings" do loader_from(:filesystem => [envdir, manifestdir, modulepath].flatten, :directory => envdir) do |loader| expect(loader.get("env1")).to environment(:env1). with_manifest(manifestdir.path). with_modulepath(modulepath.map(&:path)) end end it "does not append global_module_path to environment.conf modulepath setting" do global_path_location = File.expand_path("global_path") global_path = FS::MemoryFile.a_directory(global_path_location) loader_from(:filesystem => [envdir, manifestdir, modulepath, global_path].flatten, :directory => envdir, :modulepath => [global_path]) do |loader| expect(loader.get("env1")).to environment(:env1). with_manifest(manifestdir.path). with_modulepath(modulepath.map(&:path)) end end it "reads config_version setting" do loader_from(:filesystem => [envdir, manifestdir, modulepath].flatten, :directory => envdir) do |loader| expect(loader.get("env1")).to environment(:env1). with_manifest(manifestdir.path). with_modulepath(modulepath.map(&:path)). with_config_version(File.expand_path('/some/script')) end end it "accepts an empty environment.conf without warning" do content = nil envdir = FS::MemoryFile.a_directory(File.expand_path("envdir"), [ FS::MemoryFile.a_directory("env1", [ FS::MemoryFile.a_regular_file_containing("environment.conf", content), ]), ]) manifestdir = FS::MemoryFile.a_directory(File.join(envdir, "env1", "manifests")) modulesdir = FS::MemoryFile.a_directory(File.join(envdir, "env1", "modules")) global_path_location = File.expand_path("global_path") global_path = FS::MemoryFile.a_directory(global_path_location) loader_from(:filesystem => [envdir, manifestdir, modulesdir, global_path].flatten, :directory => envdir, :modulepath => [global_path]) do |loader| expect(loader.get("env1")).to environment(:env1). with_manifest("#{FS.path_string(envdir)}/env1/manifests"). with_modulepath(["#{FS.path_string(envdir)}/env1/modules", global_path_location]). with_config_version(nil) end expect(@logs).to be_empty end it "logs a warning, but processes the main settings if there are extraneous sections" do content << "[foo]" loader_from(:filesystem => [envdir, manifestdir, modulepath].flatten, :directory => envdir) do |loader| expect(loader.get("env1")).to environment(:env1). with_manifest(manifestdir.path). with_modulepath(modulepath.map(&:path)). with_config_version(File.expand_path('/some/script')) end expect(@logs.map(&:to_s).join).to match(/Invalid.*at.*\/env1.*may not have sections.*ignored: 'foo'/) end it "logs a warning, but processes the main settings if there are any extraneous settings" do content << "dog=arf\n" content << "cat=mew\n" content << "[ignored]\n" content << "cow=moo\n" loader_from(:filesystem => [envdir, manifestdir, modulepath].flatten, :directory => envdir) do |loader| expect(loader.get("env1")).to environment(:env1). with_manifest(manifestdir.path). with_modulepath(modulepath.map(&:path)). with_config_version(File.expand_path('/some/script')) end expect(@logs.map(&:to_s).join).to match(/Invalid.*at.*\/env1.*unknown setting.*dog, cat/) end it "interpretes relative paths from the environment's directory" do content = <<-EOF manifest=relative/manifest modulepath=relative/modules config_version=relative/script EOF envdir = FS::MemoryFile.a_directory(File.expand_path("envdir"), [ FS::MemoryFile.a_directory("env1", [ FS::MemoryFile.a_regular_file_containing("environment.conf", content), FS::MemoryFile.a_missing_file("modules"), FS::MemoryFile.a_directory('relative', [ FS::MemoryFile.a_directory('modules'), ]), ]), ]) loader_from(:filesystem => [envdir], :directory => envdir) do |loader| expect(loader.get("env1")).to environment(:env1). with_manifest(File.join(envdir, 'env1', 'relative', 'manifest')). with_modulepath([File.join(envdir, 'env1', 'relative', 'modules')]). with_config_version(File.join(envdir, 'env1', 'relative', 'script')) end end it "interpolates other setting values correctly" do modulepath = [ File.expand_path('/some/absolute'), '$basemodulepath', 'modules' ].join(File::PATH_SEPARATOR) content = <<-EOF manifest=$confdir/whackymanifests modulepath=#{modulepath} config_version=$vardir/random/scripts EOF some_absolute_dir = FS::MemoryFile.a_directory(File.expand_path('/some/absolute')) base_module_dirs = Puppet[:basemodulepath].split(File::PATH_SEPARATOR).map do |path| FS::MemoryFile.a_directory(path) end envdir = FS::MemoryFile.a_directory(File.expand_path("envdir"), [ FS::MemoryFile.a_directory("env1", [ FS::MemoryFile.a_regular_file_containing("environment.conf", content), FS::MemoryFile.a_directory("modules"), ]), ]) loader_from(:filesystem => [envdir, some_absolute_dir, base_module_dirs].flatten, :directory => envdir) do |loader| expect(loader.get("env1")).to environment(:env1). with_manifest(File.join(Puppet[:confdir], 'whackymanifests')). with_modulepath([some_absolute_dir.path, base_module_dirs.map { |d| d.path }, File.join(envdir, 'env1', 'modules')].flatten). with_config_version(File.join(Puppet[:vardir], 'random', 'scripts')) end end it "uses environment.conf settings regardless of existence of modules and manifests subdirectories" do envdir = FS::MemoryFile.a_directory(File.expand_path("envdir"), [ FS::MemoryFile.a_directory("env1", [ FS::MemoryFile.a_regular_file_containing("environment.conf", content), FS::MemoryFile.a_directory("modules"), FS::MemoryFile.a_directory("manifests"), ]), ]) loader_from(:filesystem => [envdir, manifestdir, modulepath].flatten, :directory => envdir) do |loader| expect(loader.get("env1")).to environment(:env1). with_manifest(manifestdir.path). with_modulepath(modulepath.map(&:path)). with_config_version(File.expand_path('/some/script')) end end + + it "should update environment settings if environment.conf has changed and timeout has expired" do + base_dir = File.expand_path("envdir") + original_envdir = FS::MemoryFile.a_directory(base_dir, [ + FS::MemoryFile.a_directory("env3", [ + FS::MemoryFile.a_regular_file_containing("environment.conf", <<-EOF) + manifest=/manifest_orig + modulepath=/modules_orig + environment_timeout=0 + EOF + ]), + ]) + + FS.overlay(original_envdir) do + dir_loader = Puppet::Environments::Directories.new(original_envdir, []) + loader = Puppet::Environments::Cached.new(dir_loader) + Puppet.override(:environments => loader) do + original_env = loader.get("env3") # force the environment.conf to be read + + changed_envdir = FS::MemoryFile.a_directory(base_dir, [ + FS::MemoryFile.a_directory("env3", [ + FS::MemoryFile.a_regular_file_containing("environment.conf", <<-EOF) + manifest=/manifest_changed + modulepath=/modules_changed + environment_timeout=0 + EOF + ]), + ]) + + FS.overlay(changed_envdir) do + changed_env = loader.get("env3") + + expect(original_env).to environment(:env3). + with_manifest(File.expand_path("/manifest_orig")). + with_full_modulepath([File.expand_path("/modules_orig")]) + + expect(changed_env).to environment(:env3). + with_manifest(File.expand_path("/manifest_changed")). + with_full_modulepath([File.expand_path("/modules_changed")]) + end + end + end + end end end describe "static loaders" do let(:static1) { Puppet::Node::Environment.create(:static1, []) } let(:static2) { Puppet::Node::Environment.create(:static2, []) } let(:loader) { Puppet::Environments::Static.new(static1, static2) } it "lists environments" do expect(loader.list).to eq([static1, static2]) end it "gets an environment" do expect(loader.get(:static2)).to eq(static2) end it "returns nil if env not found" do expect(loader.get(:doesnotexist)).to be_nil end it "raises error if environment is not found" do expect do loader.get!(:doesnotexist) end.to raise_error(Puppet::Environments::EnvironmentNotFound) end it "gets a basic conf" do conf = loader.get_conf(:static1) expect(conf.modulepath).to eq('') expect(conf.manifest).to eq(:no_manifest) expect(conf.config_version).to be_nil end it "returns nil if you request a configuration from an env that doesn't exist" do expect(loader.get_conf(:doesnotexist)).to be_nil end context "that are private" do let(:private_env) { Puppet::Node::Environment.create(:private, []) } let(:loader) { Puppet::Environments::StaticPrivate.new(private_env) } it "lists nothing" do expect(loader.list).to eq([]) end end end describe "cached loaders" do let(:cached1) { Puppet::Node::Environment.create(:cached1, []) } let(:cached2) { Puppet::Node::Environment.create(:cached2, []) } let(:static_loader) { Puppet::Environments::Static.new(cached1, cached2) } let(:loader) { Puppet::Environments::Cached.new(static_loader) } it "gets an environment" do expect(loader.get(:cached2)).to eq(cached2) end it "returns nil if env not found" do expect(loader.get(:doesnotexist)).to be_nil end it "raises error if environment is not found" do expect do loader.get!(:doesnotexist) end.to raise_error(Puppet::Environments::EnvironmentNotFound) end end RSpec::Matchers.define :environment do |name| match do |env| env.name == name && (!@manifest || @manifest == env.manifest) && (!@modulepath || @modulepath == env.modulepath) && + (!@full_modulepath || @full_modulepath == env.full_modulepath) && (!@config_version || @config_version == env.config_version) end chain :with_manifest do |manifest| @manifest = manifest end chain :with_modulepath do |modulepath| @modulepath = modulepath end + chain :with_full_modulepath do |full_modulepath| + @full_modulepath = full_modulepath + end + chain :with_config_version do |config_version| @config_version = config_version end description do "environment #{expected}" + (@manifest ? " with manifest #{@manifest}" : "") + (@modulepath ? " with modulepath [#{@modulepath.join(', ')}]" : "") + + (@full_modulepath ? " with full_modulepath [#{@full_modulepath.join(', ')}]" : "") + (@config_version ? " with config_version #{@config_version}" : "") end failure_message_for_should do |env| "expected <#{env.name}: modulepath = [#{env.modulepath.join(', ')}], manifest = #{env.manifest}, config_version = #{env.config_version}> to be #{description}" end end def loader_from(options, &block) FS.overlay(*options[:filesystem]) do environments = Puppet::Environments::Directories.new( options[:directory], options[:modulepath] || [] ) Puppet.override(:environments => environments) do yield environments end end end end end diff --git a/spec/unit/parser/functions/lookup_spec.rb b/spec/unit/parser/functions/lookup_spec.rb index b6d909b6f..54ea55423 100644 --- a/spec/unit/parser/functions/lookup_spec.rb +++ b/spec/unit/parser/functions/lookup_spec.rb @@ -1,146 +1,147 @@ require 'spec_helper' require 'puppet/pops' require 'stringio' require 'puppet_spec/scope' describe "lookup function" do include PuppetSpec::Scope before(:each) do Puppet[:binder] = true + Puppet[:parser] = 'future' end it "must be called with at least a name to lookup" do scope = scope_with_injections_from(bound(bindings)) expect do scope.function_lookup([]) end.to raise_error(ArgumentError, /Wrong number of arguments/) end it "looks up a value that exists" do scope = scope_with_injections_from(bound(bind_single("a_value", "something"))) expect(scope.function_lookup(['a_value'])).to eq('something') end it "searches for first found if given several names" do scope = scope_with_injections_from(bound(bind_single("a_value", "something"))) expect(scope.function_lookup([['b_value', 'a_value', 'c_value']])).to eq('something') end it "override wins over bound" do scope = scope_with_injections_from(bound(bind_single("a_value", "something"))) options = {:override => { 'a_value' => 'something_overridden' }} expect(scope.function_lookup(['a_value', options])).to eq('something_overridden') end it "extra option is used if nothing is found" do scope = scope_with_injections_from(bound(bind_single("another_value", "something"))) options = {:extra => { 'a_value' => 'something_extra' }} expect(scope.function_lookup(['a_value', options])).to eq('something_extra') end it "hiera is called to lookup if value is not bound" do Puppet::Parser::Scope.any_instance.stubs(:function_hiera).returns('from_hiera') scope = scope_with_injections_from(bound(bind_single("another_value", "something"))) expect(scope.function_lookup(['a_value'])).to eq('from_hiera') end - it "returns :undef when the requested value is not bound and undef is accepted" do + it "returns nil when the requested value is not bound and undef is accepted" do scope = scope_with_injections_from(bound(bind_single("a_value", "something"))) - expect(scope.function_lookup(['not_bound_value',{'accept_undef' => true}])).to eq(:undef) + expect(scope.function_lookup(['not_bound_value',{'accept_undef' => true}])).to eq(nil) end it "fails if the requested value is not bound and undef is not allowed" do scope = scope_with_injections_from(bound(bind_single("a_value", "something"))) expect do scope.function_lookup(['not_bound_value']) end.to raise_error(/did not find a value for the name 'not_bound_value'/) end it "returns the given default value when the requested value is not bound" do scope = scope_with_injections_from(bound(bind_single("a_value", "something"))) expect(scope.function_lookup(['not_bound_value','String', 'cigar'])).to eq('cigar') end it "accepts values given in a hash of options" do scope = scope_with_injections_from(bound(bind_single("a_value", "something"))) expect(scope.function_lookup(['not_bound_value',{'type' => 'String', 'default' => 'cigar'}])).to eq('cigar') end it "raises an error when the bound type is not assignable to the requested type" do scope = scope_with_injections_from(bound(bind_single("a_value", "something"))) expect do scope.function_lookup(['a_value', 'Integer']) end.to raise_error(ArgumentError, /incompatible type, expected: Integer, got: String/) end it "returns the value if the bound type is assignable to the requested type" do typed_bindings = bindings typed_bindings.bind().string().name("a_value").to("something") scope = scope_with_injections_from(bound(typed_bindings)) expect(scope.function_lookup(['a_value', 'Data'])).to eq("something") end it "yields to a given lambda and returns the result" do scope = scope_with_injections_from(bound(bind_single("a_value", "something"))) - expect(scope.function_lookup(['a_value', ast_lambda('|$x|{something_else}')])).to eq('something_else') + expect(scope.function_lookup(['a_value', ast_lambda(scope, '|$x|{something_else}')])).to eq('something_else') end it "fails if given lambda produces undef" do scope = scope_with_injections_from(bound(bind_single("a_value", "something"))) expect do - scope.function_lookup(['a_value', ast_lambda('|$x|{undef}')]) + scope.function_lookup(['a_value', ast_lambda(scope, '|$x|{undef}')]) end.to raise_error(/did not find a value for the name 'a_value'/) end it "yields name and result to a given lambda" do scope = scope_with_injections_from(bound(bind_single("a_value", "something"))) - expect(scope.function_lookup(['a_value', ast_lambda('|$name, $result|{[$name, $result]}')])).to eq(['a_value', 'something']) + expect(scope.function_lookup(['a_value', ast_lambda(scope, '|$name, $result|{[$name, $result]}')])).to eq(['a_value', 'something']) end it "yields name and result and default to a given lambda" do scope = scope_with_injections_from(bound(bind_single("a_value", "something"))) expect(scope.function_lookup(['a_value', {'default' => 'cigar'}, - ast_lambda('|$name, $result, $d|{[$name, $result, $d]}')])).to eq(['a_value', 'something', 'cigar']) + ast_lambda(scope, '|$name, $result, $d|{[$name, $result, $d]}')])).to eq(['a_value', 'something', 'cigar']) end it "yields to a given lambda and returns the result when giving name and type" do scope = scope_with_injections_from(bound(bind_single("a_value", "something"))) - expect(scope.function_lookup(['a_value', 'String', ast_lambda('|$x|{something_else}')])).to eq('something_else') + expect(scope.function_lookup(['a_value', 'String', ast_lambda(scope, '|$x|{something_else}')])).to eq('something_else') end it "yields :undef when value is not found and using a lambda" do scope = scope_with_injections_from(bound(bind_single("a_value", "something"))) - expect(scope.function_lookup(['not_bound_value', ast_lambda('|$x|{ if $x == undef {good} else {bad}}')])).to eq('good') + expect(scope.function_lookup(['not_bound_value', ast_lambda(scope, '|$x|{ if $x == undef {good} else {bad}}')])).to eq('good') end def scope_with_injections_from(binder) injector = Puppet::Pops::Binder::Injector.new(binder) scope = create_test_scope_for_node('testing') scope.compiler.injector = injector scope end def bindings Puppet::Pops::Binder::BindingsFactory.named_bindings("testing") end def bind_single(name, value) local_bindings = Puppet::Pops::Binder::BindingsFactory.named_bindings("testing") local_bindings.bind().name(name).to(value) local_bindings end def bound(local_bindings) layered_bindings = Puppet::Pops::Binder::BindingsFactory.layered_bindings(Puppet::Pops::Binder::BindingsFactory.named_layer('test layer', local_bindings.model)) Puppet::Pops::Binder::Binder.new(layered_bindings) end - def ast_lambda(puppet_source) + def ast_lambda(scope, puppet_source) puppet_source = "fake_func() " + puppet_source - model = Puppet::Pops::Parser::EvaluatingParser.new().parse_string(puppet_source, __FILE__).current - model = model.body.lambda - Puppet::Pops::Model::AstTransformer.new(@file_source, nil).transform(model) + evaluator = Puppet::Pops::Parser::EvaluatingParser.new() + model = evaluator.parse_string(puppet_source, __FILE__).current + evaluator.closure(model.body.lambda, scope) end end diff --git a/spec/unit/parser/resource/param_spec.rb b/spec/unit/parser/resource/param_spec.rb index 7989d060d..dcb8a3616 100755 --- a/spec/unit/parser/resource/param_spec.rb +++ b/spec/unit/parser/resource/param_spec.rb @@ -1,44 +1,32 @@ -#! /usr/bin/env ruby require 'spec_helper' describe Puppet::Parser::Resource::Param do - it "can be instantiated" do - Puppet::Parser::Resource::Param.new(:name => 'myparam', :value => 'foo') - end - - it "stores the source file" do - param = Puppet::Parser::Resource::Param.new(:name => 'myparam', :value => 'foo', :file => 'foo.pp') - param.file.should == 'foo.pp' - end + it "has readers for all of the attributes" do + param = Puppet::Parser::Resource::Param.new(:name => 'myparam', :value => 'foo', :file => 'foo.pp', :line => 42) - it "stores the line number" do - param = Puppet::Parser::Resource::Param.new(:name => 'myparam', :value => 'foo', :line => 42) - param.line.should == 42 + expect(param.name).to eq(:myparam) + expect(param.value).to eq('foo') + expect(param.file).to eq('foo.pp') + expect(param.line).to eq(42) end context "parameter validation" do it "throws an error when instantiated without a name" do expect { Puppet::Parser::Resource::Param.new(:value => 'foo') }.to raise_error(Puppet::Error, /name is a required option/) end - it "throws an error when instantiated without a value" do - expect { - Puppet::Parser::Resource::Param.new(:name => 'myparam') - }.to raise_error(Puppet::Error, /value is a required option/) - end + it "does not require a value" do + param = Puppet::Parser::Resource::Param.new(:name => 'myparam') - it "throws an error when instantiated with a nil value" do - expect { - Puppet::Parser::Resource::Param.new(:name => 'myparam', :value => nil) - }.to raise_error(Puppet::Error, /value is a required option/) + expect(param.value).to be_nil end it "includes file/line context in errors" do expect { - Puppet::Parser::Resource::Param.new(:name => 'myparam', :value => nil, :file => 'foo.pp', :line => 42) + Puppet::Parser::Resource::Param.new(:file => 'foo.pp', :line => 42) }.to raise_error(Puppet::Error, /foo.pp:42/) end end end diff --git a/spec/unit/parser/resource_spec.rb b/spec/unit/parser/resource_spec.rb index d0ec5f49c..97f19e21a 100755 --- a/spec/unit/parser/resource_spec.rb +++ b/spec/unit/parser/resource_spec.rb @@ -1,586 +1,582 @@ require 'spec_helper' describe Puppet::Parser::Resource do before do environment = Puppet::Node::Environment.create(:testing, []) @node = Puppet::Node.new("yaynode", :environment => environment) @known_resource_types = environment.known_resource_types @compiler = Puppet::Parser::Compiler.new(@node) @source = newclass "" @scope = @compiler.topscope end def mkresource(args = {}) args[:source] ||= @source args[:scope] ||= @scope params = args[:parameters] || {:one => "yay", :three => "rah"} if args[:parameters] == :none args.delete(:parameters) elsif not args[:parameters].is_a? Array args[:parameters] = paramify(args[:source], params) end Puppet::Parser::Resource.new("resource", "testing", args) end def param(name, value, source) Puppet::Parser::Resource::Param.new(:name => name, :value => value, :source => source) end def paramify(source, hash) hash.collect do |name, value| Puppet::Parser::Resource::Param.new( :name => name, :value => value, :source => source ) end end def newclass(name) @known_resource_types.add Puppet::Resource::Type.new(:hostclass, name) end def newdefine(name) @known_resource_types.add Puppet::Resource::Type.new(:definition, name) end def newnode(name) @known_resource_types.add Puppet::Resource::Type.new(:node, name) end it "should get its environment from its scope" do scope = stub 'scope', :source => stub("source"), :namespaces => nil scope.expects(:environment).returns("foo").at_least_once Puppet::Parser::Resource.new("file", "whatever", :scope => scope).environment.should == "foo" end it "should use the resource type collection helper module" do Puppet::Parser::Resource.ancestors.should be_include(Puppet::Resource::TypeCollectionHelper) end it "should use the scope's environment as its environment" do @scope.expects(:environment).returns("myenv").at_least_once Puppet::Parser::Resource.new("file", "whatever", :scope => @scope).environment.should == "myenv" end it "should be isomorphic if it is builtin and models an isomorphic type" do Puppet::Type.type(:file).expects(:isomorphic?).returns(true) @resource = Puppet::Parser::Resource.new("file", "whatever", :scope => @scope, :source => @source).isomorphic?.should be_true end it "should not be isomorphic if it is builtin and models a non-isomorphic type" do Puppet::Type.type(:file).expects(:isomorphic?).returns(false) @resource = Puppet::Parser::Resource.new("file", "whatever", :scope => @scope, :source => @source).isomorphic?.should be_false end it "should be isomorphic if it is not builtin" do newdefine "whatever" @resource = Puppet::Parser::Resource.new("whatever", "whatever", :scope => @scope, :source => @source).isomorphic?.should be_true end it "should have an array-indexing method for retrieving parameter values" do @resource = mkresource @resource[:one].should == "yay" end it "should use a Puppet::Resource for converting to a ral resource" do trans = mock 'resource', :to_ral => "yay" @resource = mkresource @resource.expects(:copy_as_resource).returns trans @resource.to_ral.should == "yay" end it "should be able to use the indexing operator to access parameters" do resource = Puppet::Parser::Resource.new("resource", "testing", :source => "source", :scope => @scope) resource["foo"] = "bar" resource["foo"].should == "bar" end it "should return the title when asked for a parameter named 'title'" do Puppet::Parser::Resource.new("resource", "testing", :source => @source, :scope => @scope)[:title].should == "testing" end describe "when initializing" do before do @arguments = {:scope => @scope} end it "should fail unless #{name.to_s} is specified" do expect { Puppet::Parser::Resource.new('file', '/my/file') }.to raise_error(ArgumentError, /Resources require a hash as last argument/) end it "should set the reference correctly" do res = Puppet::Parser::Resource.new("resource", "testing", @arguments) res.ref.should == "Resource[testing]" end it "should be tagged with user tags" do tags = [ "tag1", "tag2" ] @arguments[:parameters] = [ param(:tag, tags , :source) ] res = Puppet::Parser::Resource.new("resource", "testing", @arguments) res.should be_tagged("tag1") res.should be_tagged("tag2") end end describe "when evaluating" do before do @catalog = Puppet::Resource::Catalog.new source = stub('source') source.stubs(:module_name) @scope = Puppet::Parser::Scope.new(@compiler, :source => source) @catalog.add_resource(Puppet::Parser::Resource.new("stage", :main, :scope => @scope)) end it "should evaluate the associated AST definition" do definition = newdefine "mydefine" res = Puppet::Parser::Resource.new("mydefine", "whatever", :scope => @scope, :source => @source, :catalog => @catalog) definition.expects(:evaluate_code).with(res) res.evaluate end it "should evaluate the associated AST class" do @class = newclass "myclass" res = Puppet::Parser::Resource.new("class", "myclass", :scope => @scope, :source => @source, :catalog => @catalog) @class.expects(:evaluate_code).with(res) res.evaluate end it "should evaluate the associated AST node" do nodedef = newnode("mynode") res = Puppet::Parser::Resource.new("node", "mynode", :scope => @scope, :source => @source, :catalog => @catalog) nodedef.expects(:evaluate_code).with(res) res.evaluate end it "should add an edge to any specified stage for class resources" do @compiler.known_resource_types.add Puppet::Resource::Type.new(:hostclass, "foo", {}) other_stage = Puppet::Parser::Resource.new(:stage, "other", :scope => @scope, :catalog => @catalog) @compiler.add_resource(@scope, other_stage) resource = Puppet::Parser::Resource.new(:class, "foo", :scope => @scope, :catalog => @catalog) resource[:stage] = 'other' @compiler.add_resource(@scope, resource) resource.evaluate @compiler.catalog.edge?(other_stage, resource).should be_true end it "should fail if an unknown stage is specified" do @compiler.known_resource_types.add Puppet::Resource::Type.new(:hostclass, "foo", {}) resource = Puppet::Parser::Resource.new(:class, "foo", :scope => @scope, :catalog => @catalog) resource[:stage] = 'other' expect { resource.evaluate }.to raise_error(ArgumentError, /Could not find stage other specified by/) end it "should add edges from the class resources to the parent's stage if no stage is specified" do main = @compiler.catalog.resource(:stage, :main) foo_stage = Puppet::Parser::Resource.new(:stage, :foo_stage, :scope => @scope, :catalog => @catalog) @compiler.add_resource(@scope, foo_stage) @compiler.known_resource_types.add Puppet::Resource::Type.new(:hostclass, "foo", {}) resource = Puppet::Parser::Resource.new(:class, "foo", :scope => @scope, :catalog => @catalog) resource[:stage] = 'foo_stage' @compiler.add_resource(@scope, resource) resource.evaluate @compiler.catalog.should be_edge(foo_stage, resource) end it "should allow edges to propagate multiple levels down the scope hierarchy" do Puppet[:code] = <<-MANIFEST stage { before: before => Stage[main] } class alpha { include beta } class beta { include gamma } class gamma { } class { alpha: stage => before } MANIFEST catalog = Puppet::Parser::Compiler.compile(Puppet::Node.new 'anyone') # Stringify them to make for easier lookup edges = catalog.edges.map {|e| [e.source.ref, e.target.ref]} edges.should include(["Stage[before]", "Class[Alpha]"]) edges.should include(["Stage[before]", "Class[Beta]"]) edges.should include(["Stage[before]", "Class[Gamma]"]) end it "should use the specified stage even if the parent scope specifies one" do Puppet[:code] = <<-MANIFEST stage { before: before => Stage[main], } stage { after: require => Stage[main], } class alpha { class { beta: stage => after } } class beta { } class { alpha: stage => before } MANIFEST catalog = Puppet::Parser::Compiler.compile(Puppet::Node.new 'anyone') edges = catalog.edges.map {|e| [e.source.ref, e.target.ref]} edges.should include(["Stage[before]", "Class[Alpha]"]) edges.should include(["Stage[after]", "Class[Beta]"]) end it "should add edges from top-level class resources to the main stage if no stage is specified" do main = @compiler.catalog.resource(:stage, :main) @compiler.known_resource_types.add Puppet::Resource::Type.new(:hostclass, "foo", {}) resource = Puppet::Parser::Resource.new(:class, "foo", :scope => @scope, :catalog => @catalog) @compiler.add_resource(@scope, resource) resource.evaluate @compiler.catalog.should be_edge(main, resource) end end describe "when finishing" do before do @class = newclass "myclass" @nodedef = newnode("mynode") @resource = Puppet::Parser::Resource.new("file", "whatever", :scope => @scope, :source => @source) end it "should do nothing if it has already been finished" do @resource.finish @resource.expects(:add_defaults).never @resource.finish end it "should add all defaults available from the scope" do @resource.scope.expects(:lookupdefaults).with(@resource.type).returns(:owner => param(:owner, "default", @resource.source)) @resource.finish @resource[:owner].should == "default" end it "should not replace existing parameters with defaults" do @resource.set_parameter :owner, "oldvalue" @resource.scope.expects(:lookupdefaults).with(@resource.type).returns(:owner => :replaced) @resource.finish @resource[:owner].should == "oldvalue" end it "should add a copy of each default, rather than the actual default parameter instance" do newparam = param(:owner, "default", @resource.source) other = newparam.dup other.value = "other" newparam.expects(:dup).returns(other) @resource.scope.expects(:lookupdefaults).with(@resource.type).returns(:owner => newparam) @resource.finish @resource[:owner].should == "other" end end describe "when being tagged" do before do @scope_resource = stub 'scope_resource', :tags => %w{srone srtwo} @scope.stubs(:resource).returns @scope_resource @resource = Puppet::Parser::Resource.new("file", "yay", :scope => @scope, :source => mock('source')) end it "should get tagged with the resource type" do @resource.tags.should be_include("file") end it "should get tagged with the title" do @resource.tags.should be_include("yay") end it "should get tagged with each name in the title if the title is a qualified class name" do resource = Puppet::Parser::Resource.new("file", "one::two", :scope => @scope, :source => mock('source')) resource.tags.should be_include("one") resource.tags.should be_include("two") end it "should get tagged with each name in the type if the type is a qualified class name" do resource = Puppet::Parser::Resource.new("one::two", "whatever", :scope => @scope, :source => mock('source')) resource.tags.should be_include("one") resource.tags.should be_include("two") end it "should not get tagged with non-alphanumeric titles" do resource = Puppet::Parser::Resource.new("file", "this is a test", :scope => @scope, :source => mock('source')) resource.tags.should_not be_include("this is a test") end it "should fail on tags containing '*' characters" do expect { @resource.tag("bad*tag") }.to raise_error(Puppet::ParseError) end it "should fail on tags starting with '-' characters" do expect { @resource.tag("-badtag") }.to raise_error(Puppet::ParseError) end it "should fail on tags containing ' ' characters" do expect { @resource.tag("bad tag") }.to raise_error(Puppet::ParseError) end it "should allow alpha tags" do expect { @resource.tag("good_tag") }.to_not raise_error end end describe "when merging overrides" do before do @source = "source1" @resource = mkresource :source => @source @override = mkresource :source => @source end it "should fail when the override was not created by a parent class" do @override.source = "source2" @override.source.expects(:child_of?).with("source1").returns(false) expect { @resource.merge(@override) }.to raise_error(Puppet::ParseError) end it "should succeed when the override was created in the current scope" do @resource.source = "source3" @override.source = @resource.source @override.source.expects(:child_of?).with("source3").never params = {:a => :b, :c => :d} @override.expects(:parameters).returns(params) @resource.expects(:override_parameter).with(:b) @resource.expects(:override_parameter).with(:d) @resource.merge(@override) end it "should succeed when a parent class created the override" do @resource.source = "source3" @override.source = "source4" @override.source.expects(:child_of?).with("source3").returns(true) params = {:a => :b, :c => :d} @override.expects(:parameters).returns(params) @resource.expects(:override_parameter).with(:b) @resource.expects(:override_parameter).with(:d) @resource.merge(@override) end it "should add new parameters when the parameter is not set" do @source.stubs(:child_of?).returns true @override.set_parameter(:testing, "value") @resource.merge(@override) @resource[:testing].should == "value" end it "should replace existing parameter values" do @source.stubs(:child_of?).returns true @resource.set_parameter(:testing, "old") @override.set_parameter(:testing, "value") @resource.merge(@override) @resource[:testing].should == "value" end it "should add values to the parameter when the override was created with the '+>' syntax" do @source.stubs(:child_of?).returns true param = Puppet::Parser::Resource::Param.new(:name => :testing, :value => "testing", :source => @resource.source) param.add = true @override.set_parameter(param) @resource.set_parameter(:testing, "other") @resource.merge(@override) @resource[:testing].should == %w{other testing} end it "should not merge parameter values when multiple resources are overriden with '+>' at once " do @resource_2 = mkresource :source => @source @resource. set_parameter(:testing, "old_val_1") @resource_2.set_parameter(:testing, "old_val_2") @source.stubs(:child_of?).returns true param = Puppet::Parser::Resource::Param.new(:name => :testing, :value => "new_val", :source => @resource.source) param.add = true @override.set_parameter(param) @resource. merge(@override) @resource_2.merge(@override) @resource [:testing].should == %w{old_val_1 new_val} @resource_2[:testing].should == %w{old_val_2 new_val} end it "should promote tag overrides to real tags" do @source.stubs(:child_of?).returns true param = Puppet::Parser::Resource::Param.new(:name => :tag, :value => "testing", :source => @resource.source) @override.set_parameter(param) @resource.merge(@override) @resource.tagged?("testing").should be_true end end it "should be able to be converted to a normal resource" do @source = stub 'scope', :name => "myscope" @resource = mkresource :source => @source @resource.should respond_to(:copy_as_resource) end describe "when being converted to a resource" do before do @parser_resource = mkresource :scope => @scope, :parameters => {:foo => "bar", :fee => "fum"} end it "should create an instance of Puppet::Resource" do @parser_resource.copy_as_resource.should be_instance_of(Puppet::Resource) end it "should set the type correctly on the Puppet::Resource" do @parser_resource.copy_as_resource.type.should == @parser_resource.type end it "should set the title correctly on the Puppet::Resource" do @parser_resource.copy_as_resource.title.should == @parser_resource.title end it "should copy over all of the parameters" do result = @parser_resource.copy_as_resource.to_hash # The name will be in here, also. result[:foo].should == "bar" result[:fee].should == "fum" end it "should copy over the tags" do @parser_resource.tag "foo" @parser_resource.tag "bar" @parser_resource.copy_as_resource.tags.should == @parser_resource.tags end it "should copy over the line" do @parser_resource.line = 40 @parser_resource.copy_as_resource.line.should == 40 end it "should copy over the file" do @parser_resource.file = "/my/file" @parser_resource.copy_as_resource.file.should == "/my/file" end it "should copy over the 'exported' value" do @parser_resource.exported = true @parser_resource.copy_as_resource.exported.should be_true end it "should copy over the 'virtual' value" do @parser_resource.virtual = true @parser_resource.copy_as_resource.virtual.should be_true end it "should convert any parser resource references to Puppet::Resource instances" do ref = Puppet::Resource.new("file", "/my/file") @parser_resource = mkresource :source => @source, :parameters => {:foo => "bar", :fee => ref} result = @parser_resource.copy_as_resource result[:fee].should == Puppet::Resource.new(:file, "/my/file") end it "should convert any parser resource references to Puppet::Resource instances even if they are in an array" do ref = Puppet::Resource.new("file", "/my/file") @parser_resource = mkresource :source => @source, :parameters => {:foo => "bar", :fee => ["a", ref]} result = @parser_resource.copy_as_resource result[:fee].should == ["a", Puppet::Resource.new(:file, "/my/file")] end it "should convert any parser resource references to Puppet::Resource instances even if they are in an array of array, and even deeper" do ref1 = Puppet::Resource.new("file", "/my/file1") ref2 = Puppet::Resource.new("file", "/my/file2") @parser_resource = mkresource :source => @source, :parameters => {:foo => "bar", :fee => ["a", [ref1,ref2]]} result = @parser_resource.copy_as_resource result[:fee].should == ["a", Puppet::Resource.new(:file, "/my/file1"), Puppet::Resource.new(:file, "/my/file2")] end it "should fail if the same param is declared twice" do lambda do @parser_resource = mkresource :source => @source, :parameters => [ Puppet::Parser::Resource::Param.new( :name => :foo, :value => "bar", :source => @source ), Puppet::Parser::Resource::Param.new( :name => :foo, :value => "baz", :source => @source ) ] end.should raise_error(Puppet::ParseError) end end describe "when validating" do it "should check each parameter" do resource = Puppet::Parser::Resource.new :foo, "bar", :scope => @scope, :source => stub("source") resource[:one] = :two resource[:three] = :four resource.expects(:validate_parameter).with(:one) resource.expects(:validate_parameter).with(:three) resource.send(:validate) end it "should raise a parse error when there's a failure" do resource = Puppet::Parser::Resource.new :foo, "bar", :scope => @scope, :source => stub("source") resource[:one] = :two resource.expects(:validate_parameter).with(:one).raises ArgumentError expect { resource.send(:validate) }.to raise_error(Puppet::ParseError) end end describe "when setting parameters" do before do @source = newclass "foobar" @resource = Puppet::Parser::Resource.new :foo, "bar", :scope => @scope, :source => @source end it "should accept Param instances and add them to the parameter list" do param = Puppet::Parser::Resource::Param.new :name => "foo", :value => "bar", :source => @source @resource.set_parameter(param) @resource["foo"].should == "bar" end - it "should fail when provided a parameter name but no value" do - expect { @resource.set_parameter("myparam") }.to raise_error(ArgumentError) - end - it "should allow parameters to be set to 'false'" do @resource.set_parameter("myparam", false) @resource["myparam"].should be_false end it "should use its source when provided a parameter name and value" do @resource.set_parameter("myparam", "myvalue") @resource["myparam"].should == "myvalue" end end # part of #629 -- the undef keyword. Make sure 'undef' params get skipped. it "should not include 'undef' parameters when converting itself to a hash" do resource = Puppet::Parser::Resource.new "file", "/tmp/testing", :source => mock("source"), :scope => mock("scope") resource[:owner] = :undef resource[:mode] = "755" resource.to_hash[:owner].should be_nil end end diff --git a/spec/unit/pops/types/type_calculator_spec.rb b/spec/unit/pops/types/type_calculator_spec.rb index b11ed23a9..bb0c890a8 100644 --- a/spec/unit/pops/types/type_calculator_spec.rb +++ b/spec/unit/pops/types/type_calculator_spec.rb @@ -1,1831 +1,1861 @@ require 'spec_helper' require 'puppet/pops' describe 'The type calculator' do let(:calculator) { Puppet::Pops::Types::TypeCalculator.new() } def range_t(from, to) t = Puppet::Pops::Types::PIntegerType.new t.from = from t.to = to t end def constrained_t(t, from, to) Puppet::Pops::Types::TypeFactory.constrain_size(t, from, to) end def pattern_t(*patterns) Puppet::Pops::Types::TypeFactory.pattern(*patterns) end def regexp_t(pattern) Puppet::Pops::Types::TypeFactory.regexp(pattern) end def string_t(*strings) Puppet::Pops::Types::TypeFactory.string(*strings) end def callable_t(*params) Puppet::Pops::Types::TypeFactory.callable(*params) end def all_callables_t(*params) Puppet::Pops::Types::TypeFactory.all_callables() end def with_block_t(callable_t, *params) Puppet::Pops::Types::TypeFactory.with_block(callable_t, *params) end def with_optional_block_t(callable_t, *params) Puppet::Pops::Types::TypeFactory.with_optional_block(callable_t, *params) end def enum_t(*strings) Puppet::Pops::Types::TypeFactory.enum(*strings) end def variant_t(*types) Puppet::Pops::Types::TypeFactory.variant(*types) end def integer_t() Puppet::Pops::Types::TypeFactory.integer() end def array_t(t) Puppet::Pops::Types::TypeFactory.array_of(t) end def hash_t(k,v) Puppet::Pops::Types::TypeFactory.hash_of(v, k) end def data_t() Puppet::Pops::Types::TypeFactory.data() end def factory() Puppet::Pops::Types::TypeFactory end def collection_t() Puppet::Pops::Types::TypeFactory.collection() end def tuple_t(*types) Puppet::Pops::Types::TypeFactory.tuple(*types) end def struct_t(type_hash) Puppet::Pops::Types::TypeFactory.struct(type_hash) end def object_t Puppet::Pops::Types::TypeFactory.any() end def unit_t # Cannot be created via factory, the type is private to the type system Puppet::Pops::Types::PUnitType.new end def types Puppet::Pops::Types end shared_context "types_setup" do # Do not include the special type Unit in this list def all_types [ Puppet::Pops::Types::PAnyType, Puppet::Pops::Types::PNilType, Puppet::Pops::Types::PDataType, Puppet::Pops::Types::PScalarType, Puppet::Pops::Types::PStringType, Puppet::Pops::Types::PNumericType, Puppet::Pops::Types::PIntegerType, Puppet::Pops::Types::PFloatType, Puppet::Pops::Types::PRegexpType, Puppet::Pops::Types::PBooleanType, Puppet::Pops::Types::PCollectionType, Puppet::Pops::Types::PArrayType, Puppet::Pops::Types::PHashType, Puppet::Pops::Types::PRuntimeType, Puppet::Pops::Types::PHostClassType, Puppet::Pops::Types::PResourceType, Puppet::Pops::Types::PPatternType, Puppet::Pops::Types::PEnumType, Puppet::Pops::Types::PVariantType, Puppet::Pops::Types::PStructType, Puppet::Pops::Types::PTupleType, Puppet::Pops::Types::PCallableType, Puppet::Pops::Types::PType, Puppet::Pops::Types::POptionalType, Puppet::Pops::Types::PDefaultType, ] end def scalar_types # PVariantType is also scalar, if its types are all Scalar [ Puppet::Pops::Types::PScalarType, Puppet::Pops::Types::PStringType, Puppet::Pops::Types::PNumericType, Puppet::Pops::Types::PIntegerType, Puppet::Pops::Types::PFloatType, Puppet::Pops::Types::PRegexpType, Puppet::Pops::Types::PBooleanType, Puppet::Pops::Types::PPatternType, Puppet::Pops::Types::PEnumType, ] end def numeric_types # PVariantType is also numeric, if its types are all numeric [ Puppet::Pops::Types::PNumericType, Puppet::Pops::Types::PIntegerType, Puppet::Pops::Types::PFloatType, ] end def string_types # PVariantType is also string type, if its types are all compatible [ Puppet::Pops::Types::PStringType, Puppet::Pops::Types::PPatternType, Puppet::Pops::Types::PEnumType, ] end def collection_types # PVariantType is also string type, if its types are all compatible [ Puppet::Pops::Types::PCollectionType, Puppet::Pops::Types::PHashType, Puppet::Pops::Types::PArrayType, Puppet::Pops::Types::PStructType, Puppet::Pops::Types::PTupleType, ] end def data_compatible_types result = scalar_types result << Puppet::Pops::Types::PDataType result << array_t(types::PDataType.new) result << types::TypeFactory.hash_of_data result << Puppet::Pops::Types::PNilType tmp = tuple_t(types::PDataType.new) result << (tmp) tmp.size_type = range_t(0, nil) result end def type_from_class(c) c.is_a?(Class) ? c.new : c end end context 'when inferring ruby' do it 'fixnum translates to PIntegerType' do calculator.infer(1).class.should == Puppet::Pops::Types::PIntegerType end it 'large fixnum (or bignum depending on architecture) translates to PIntegerType' do calculator.infer(2**33).class.should == Puppet::Pops::Types::PIntegerType end it 'float translates to PFloatType' do calculator.infer(1.3).class.should == Puppet::Pops::Types::PFloatType end it 'string translates to PStringType' do calculator.infer('foo').class.should == Puppet::Pops::Types::PStringType end it 'inferred string type knows the string value' do t = calculator.infer('foo') t.class.should == Puppet::Pops::Types::PStringType t.values.should == ['foo'] end it 'boolean true translates to PBooleanType' do calculator.infer(true).class.should == Puppet::Pops::Types::PBooleanType end it 'boolean false translates to PBooleanType' do calculator.infer(false).class.should == Puppet::Pops::Types::PBooleanType end it 'regexp translates to PRegexpType' do calculator.infer(/^a regular expression$/).class.should == Puppet::Pops::Types::PRegexpType end it 'nil translates to PNilType' do calculator.infer(nil).class.should == Puppet::Pops::Types::PNilType end it ':undef translates to PRuntimeType' do calculator.infer(:undef).class.should == Puppet::Pops::Types::PRuntimeType end it 'an instance of class Foo translates to PRuntimeType[ruby, Foo]' do class Foo end t = calculator.infer(Foo.new) t.class.should == Puppet::Pops::Types::PRuntimeType t.runtime.should == :ruby t.runtime_type_name.should == 'Foo' end context 'array' do it 'translates to PArrayType' do calculator.infer([1,2]).class.should == Puppet::Pops::Types::PArrayType end it 'with fixnum values translates to PArrayType[PIntegerType]' do calculator.infer([1,2]).element_type.class.should == Puppet::Pops::Types::PIntegerType end it 'with 32 and 64 bit integer values translates to PArrayType[PIntegerType]' do calculator.infer([1,2**33]).element_type.class.should == Puppet::Pops::Types::PIntegerType end it 'Range of integer values are computed' do t = calculator.infer([-3,0,42]).element_type t.class.should == Puppet::Pops::Types::PIntegerType t.from.should == -3 t.to.should == 42 end it "Compound string values are computed" do t = calculator.infer(['a','b', 'c']).element_type t.class.should == Puppet::Pops::Types::PStringType t.values.should == ['a', 'b', 'c'] end it 'with fixnum and float values translates to PArrayType[PNumericType]' do calculator.infer([1,2.0]).element_type.class.should == Puppet::Pops::Types::PNumericType end it 'with fixnum and string values translates to PArrayType[PScalarType]' do calculator.infer([1,'two']).element_type.class.should == Puppet::Pops::Types::PScalarType end it 'with float and string values translates to PArrayType[PScalarType]' do calculator.infer([1.0,'two']).element_type.class.should == Puppet::Pops::Types::PScalarType end it 'with fixnum, float, and string values translates to PArrayType[PScalarType]' do calculator.infer([1, 2.0,'two']).element_type.class.should == Puppet::Pops::Types::PScalarType end it 'with fixnum and regexp values translates to PArrayType[PScalarType]' do calculator.infer([1, /two/]).element_type.class.should == Puppet::Pops::Types::PScalarType end it 'with string and regexp values translates to PArrayType[PScalarType]' do calculator.infer(['one', /two/]).element_type.class.should == Puppet::Pops::Types::PScalarType end it 'with string and symbol values translates to PArrayType[PAnyType]' do calculator.infer(['one', :two]).element_type.class.should == Puppet::Pops::Types::PAnyType end it 'with fixnum and nil values translates to PArrayType[PIntegerType]' do calculator.infer([1, nil]).element_type.class.should == Puppet::Pops::Types::PIntegerType end it 'with arrays of string values translates to PArrayType[PArrayType[PStringType]]' do et = calculator.infer([['first' 'array'], ['second','array']]) et.class.should == Puppet::Pops::Types::PArrayType et = et.element_type et.class.should == Puppet::Pops::Types::PArrayType et = et.element_type et.class.should == Puppet::Pops::Types::PStringType end it 'with array of string values and array of fixnums translates to PArrayType[PArrayType[PScalarType]]' do et = calculator.infer([['first' 'array'], [1,2]]) et.class.should == Puppet::Pops::Types::PArrayType et = et.element_type et.class.should == Puppet::Pops::Types::PArrayType et = et.element_type et.class.should == Puppet::Pops::Types::PScalarType end it 'with hashes of string values translates to PArrayType[PHashType[PStringType]]' do et = calculator.infer([{:first => 'first', :second => 'second' }, {:first => 'first', :second => 'second' }]) et.class.should == Puppet::Pops::Types::PArrayType et = et.element_type et.class.should == Puppet::Pops::Types::PHashType et = et.element_type et.class.should == Puppet::Pops::Types::PStringType end it 'with hash of string values and hash of fixnums translates to PArrayType[PHashType[PScalarType]]' do et = calculator.infer([{:first => 'first', :second => 'second' }, {:first => 1, :second => 2 }]) et.class.should == Puppet::Pops::Types::PArrayType et = et.element_type et.class.should == Puppet::Pops::Types::PHashType et = et.element_type et.class.should == Puppet::Pops::Types::PScalarType end end context 'hash' do it 'translates to PHashType' do calculator.infer({:first => 1, :second => 2}).class.should == Puppet::Pops::Types::PHashType end it 'with symbolic keys translates to PHashType[PRuntimeType[ruby, Symbol], value]' do k = calculator.infer({:first => 1, :second => 2}).key_type k.class.should == Puppet::Pops::Types::PRuntimeType k.runtime.should == :ruby k.runtime_type_name.should == 'Symbol' end it 'with string keys translates to PHashType[PStringType, value]' do calculator.infer({'first' => 1, 'second' => 2}).key_type.class.should == Puppet::Pops::Types::PStringType end it 'with fixnum values translates to PHashType[key, PIntegerType]' do calculator.infer({:first => 1, :second => 2}).element_type.class.should == Puppet::Pops::Types::PIntegerType end end end context 'patterns' do it "constructs a PPatternType" do t = pattern_t('a(b)c') t.class.should == Puppet::Pops::Types::PPatternType t.patterns.size.should == 1 t.patterns[0].class.should == Puppet::Pops::Types::PRegexpType t.patterns[0].pattern.should == 'a(b)c' t.patterns[0].regexp.match('abc')[1].should == 'b' end it "constructs a PStringType with multiple strings" do t = string_t('a', 'b', 'c', 'abc') t.values.should == ['a', 'b', 'c', 'abc'] end end # Deal with cases not covered by computing common type context 'when computing common type' do it 'computes given resource type commonality' do r1 = Puppet::Pops::Types::PResourceType.new() r1.type_name = 'File' r2 = Puppet::Pops::Types::PResourceType.new() r2.type_name = 'File' calculator.string(calculator.common_type(r1, r2)).should == "File" r2 = Puppet::Pops::Types::PResourceType.new() r2.type_name = 'File' r2.title = '/tmp/foo' calculator.string(calculator.common_type(r1, r2)).should == "File" r1 = Puppet::Pops::Types::PResourceType.new() r1.type_name = 'File' r1.title = '/tmp/foo' calculator.string(calculator.common_type(r1, r2)).should == "File['/tmp/foo']" r1 = Puppet::Pops::Types::PResourceType.new() r1.type_name = 'File' r1.title = '/tmp/bar' calculator.string(calculator.common_type(r1, r2)).should == "File" r2 = Puppet::Pops::Types::PResourceType.new() r2.type_name = 'Package' r2.title = 'apache' calculator.string(calculator.common_type(r1, r2)).should == "Resource" end it 'computes given hostclass type commonality' do r1 = Puppet::Pops::Types::PHostClassType.new() r1.class_name = 'foo' r2 = Puppet::Pops::Types::PHostClassType.new() r2.class_name = 'foo' calculator.string(calculator.common_type(r1, r2)).should == "Class[foo]" r2 = Puppet::Pops::Types::PHostClassType.new() r2.class_name = 'bar' calculator.string(calculator.common_type(r1, r2)).should == "Class" r2 = Puppet::Pops::Types::PHostClassType.new() calculator.string(calculator.common_type(r1, r2)).should == "Class" r1 = Puppet::Pops::Types::PHostClassType.new() calculator.string(calculator.common_type(r1, r2)).should == "Class" end it 'computes pattern commonality' do t1 = pattern_t('abc') t2 = pattern_t('xyz') common_t = calculator.common_type(t1,t2) common_t.class.should == Puppet::Pops::Types::PPatternType common_t.patterns.map { |pr| pr.pattern }.should == ['abc', 'xyz'] calculator.string(common_t).should == "Pattern[/abc/, /xyz/]" end it 'computes enum commonality to value set sum' do t1 = enum_t('a', 'b', 'c') t2 = enum_t('x', 'y', 'z') common_t = calculator.common_type(t1, t2) common_t.should == enum_t('a', 'b', 'c', 'x', 'y', 'z') end it 'computed variant commonality to type union where added types are not sub-types' do a_t1 = integer_t() a_t2 = enum_t('b') v_a = variant_t(a_t1, a_t2) b_t1 = enum_t('a') v_b = variant_t(b_t1) common_t = calculator.common_type(v_a, v_b) common_t.class.should == Puppet::Pops::Types::PVariantType Set.new(common_t.types).should == Set.new([a_t1, a_t2, b_t1]) end it 'computed variant commonality to type union where added types are sub-types' do a_t1 = integer_t() a_t2 = string_t() v_a = variant_t(a_t1, a_t2) b_t1 = enum_t('a') v_b = variant_t(b_t1) common_t = calculator.common_type(v_a, v_b) common_t.class.should == Puppet::Pops::Types::PVariantType Set.new(common_t.types).should == Set.new([a_t1, a_t2]) end context "of callables" do it 'incompatible instances => generic callable' do t1 = callable_t(String) t2 = callable_t(Integer) common_t = calculator.common_type(t1, t2) expect(common_t.class).to be(Puppet::Pops::Types::PCallableType) expect(common_t.param_types).to be_nil expect(common_t.block_type).to be_nil end it 'compatible instances => the most specific' do t1 = callable_t(String) scalar_t = Puppet::Pops::Types::PScalarType.new t2 = callable_t(scalar_t) common_t = calculator.common_type(t1, t2) expect(common_t.class).to be(Puppet::Pops::Types::PCallableType) expect(common_t.param_types.class).to be(Puppet::Pops::Types::PTupleType) expect(common_t.param_types.types).to eql([string_t]) expect(common_t.block_type).to be_nil end it 'block_type is included in the check (incompatible block)' do t1 = with_block_t(callable_t(String), String) t2 = with_block_t(callable_t(String), Integer) common_t = calculator.common_type(t1, t2) expect(common_t.class).to be(Puppet::Pops::Types::PCallableType) expect(common_t.param_types).to be_nil expect(common_t.block_type).to be_nil end it 'block_type is included in the check (compatible block)' do t1 = with_block_t(callable_t(String), String) scalar_t = Puppet::Pops::Types::PScalarType.new t2 = with_block_t(callable_t(String), scalar_t) common_t = calculator.common_type(t1, t2) expect(common_t.param_types.class).to be(Puppet::Pops::Types::PTupleType) expect(common_t.block_type).to eql(callable_t(scalar_t)) end end end context 'computes assignability' do include_context "types_setup" context 'for Unit, such that' do it 'all types are assignable to Unit' do t = Puppet::Pops::Types::PUnitType.new() all_types.each { |t2| t2.new.should be_assignable_to(t) } end it 'Unit is assignable to all other types' do t = Puppet::Pops::Types::PUnitType.new() all_types.each { |t2| t.should be_assignable_to(t2.new) } end it 'Unit is assignable to Unit' do t = Puppet::Pops::Types::PUnitType.new() t2 = Puppet::Pops::Types::PUnitType.new() t.should be_assignable_to(t2) end end context "for Any, such that" do it 'all types are assignable to Any' do t = Puppet::Pops::Types::PAnyType.new() all_types.each { |t2| t2.new.should be_assignable_to(t) } end it 'Any is not assignable to anything but Any' do tested_types = all_types() - [Puppet::Pops::Types::PAnyType] t = Puppet::Pops::Types::PAnyType.new() tested_types.each { |t2| t.should_not be_assignable_to(t2.new) } end end context "for Data, such that" do it 'all scalars + array and hash are assignable to Data' do t = Puppet::Pops::Types::PDataType.new() data_compatible_types.each { |t2| type_from_class(t2).should be_assignable_to(t) } end it 'a Variant of scalar, hash, or array is assignable to Data' do t = Puppet::Pops::Types::PDataType.new() data_compatible_types.each { |t2| variant_t(type_from_class(t2)).should be_assignable_to(t) } end it 'Data is not assignable to any of its subtypes' do t = Puppet::Pops::Types::PDataType.new() types_to_test = data_compatible_types- [Puppet::Pops::Types::PDataType] types_to_test.each {|t2| t.should_not be_assignable_to(type_from_class(t2)) } end it 'Data is not assignable to a Variant of Data subtype' do t = Puppet::Pops::Types::PDataType.new() types_to_test = data_compatible_types- [Puppet::Pops::Types::PDataType] types_to_test.each { |t2| t.should_not be_assignable_to(variant_t(type_from_class(t2))) } end it 'Data is not assignable to any disjunct type' do tested_types = all_types - [Puppet::Pops::Types::PAnyType, Puppet::Pops::Types::PDataType] - scalar_types t = Puppet::Pops::Types::PDataType.new() tested_types.each {|t2| t.should_not be_assignable_to(t2.new) } end end context "for Scalar, such that" do it "all scalars are assignable to Scalar" do t = Puppet::Pops::Types::PScalarType.new() scalar_types.each {|t2| t2.new.should be_assignable_to(t) } end it 'Scalar is not assignable to any of its subtypes' do t = Puppet::Pops::Types::PScalarType.new() types_to_test = scalar_types - [Puppet::Pops::Types::PScalarType] types_to_test.each {|t2| t.should_not be_assignable_to(t2.new) } end it 'Scalar is not assignable to any disjunct type' do tested_types = all_types - [Puppet::Pops::Types::PAnyType, Puppet::Pops::Types::PDataType] - scalar_types t = Puppet::Pops::Types::PScalarType.new() tested_types.each {|t2| t.should_not be_assignable_to(t2.new) } end end context "for Numeric, such that" do it "all numerics are assignable to Numeric" do t = Puppet::Pops::Types::PNumericType.new() numeric_types.each {|t2| t2.new.should be_assignable_to(t) } end it 'Numeric is not assignable to any of its subtypes' do t = Puppet::Pops::Types::PNumericType.new() types_to_test = numeric_types - [Puppet::Pops::Types::PNumericType] types_to_test.each {|t2| t.should_not be_assignable_to(t2.new) } end it 'Numeric is not assignable to any disjunct type' do tested_types = all_types - [ Puppet::Pops::Types::PAnyType, Puppet::Pops::Types::PDataType, Puppet::Pops::Types::PScalarType, ] - numeric_types t = Puppet::Pops::Types::PNumericType.new() tested_types.each {|t2| t.should_not be_assignable_to(t2.new) } end end context "for Collection, such that" do it "all collections are assignable to Collection" do t = Puppet::Pops::Types::PCollectionType.new() collection_types.each {|t2| t2.new.should be_assignable_to(t) } end it 'Collection is not assignable to any of its subtypes' do t = Puppet::Pops::Types::PCollectionType.new() types_to_test = collection_types - [Puppet::Pops::Types::PCollectionType] types_to_test.each {|t2| t.should_not be_assignable_to(t2.new) } end it 'Collection is not assignable to any disjunct type' do tested_types = all_types - [Puppet::Pops::Types::PAnyType] - collection_types t = Puppet::Pops::Types::PCollectionType.new() tested_types.each {|t2| t.should_not be_assignable_to(t2.new) } end end context "for Array, such that" do it "Array is not assignable to non Array based Collection type" do t = Puppet::Pops::Types::PArrayType.new() tested_types = collection_types - [ Puppet::Pops::Types::PCollectionType, Puppet::Pops::Types::PArrayType, Puppet::Pops::Types::PTupleType] tested_types.each {|t2| t.should_not be_assignable_to(t2.new) } end it 'Array is not assignable to any disjunct type' do tested_types = all_types - [ Puppet::Pops::Types::PAnyType, Puppet::Pops::Types::PDataType] - collection_types t = Puppet::Pops::Types::PArrayType.new() tested_types.each {|t2| t.should_not be_assignable_to(t2.new) } end end context "for Hash, such that" do it "Hash is not assignable to any other Collection type" do t = Puppet::Pops::Types::PHashType.new() tested_types = collection_types - [ Puppet::Pops::Types::PCollectionType, Puppet::Pops::Types::PStructType, Puppet::Pops::Types::PHashType] tested_types.each {|t2| t.should_not be_assignable_to(t2.new) } end it 'Hash is not assignable to any disjunct type' do tested_types = all_types - [ Puppet::Pops::Types::PAnyType, Puppet::Pops::Types::PDataType] - collection_types t = Puppet::Pops::Types::PHashType.new() tested_types.each {|t2| t.should_not be_assignable_to(t2.new) } end end context "for Tuple, such that" do it "Tuple is not assignable to any other non Array based Collection type" do t = Puppet::Pops::Types::PTupleType.new() tested_types = collection_types - [ Puppet::Pops::Types::PCollectionType, Puppet::Pops::Types::PTupleType, Puppet::Pops::Types::PArrayType] tested_types.each {|t2| t.should_not be_assignable_to(t2.new) } end it 'Tuple is not assignable to any disjunct type' do tested_types = all_types - [ Puppet::Pops::Types::PAnyType, Puppet::Pops::Types::PDataType] - collection_types t = Puppet::Pops::Types::PTupleType.new() tested_types.each {|t2| t.should_not be_assignable_to(t2.new) } end end context "for Struct, such that" do it "Struct is not assignable to any other non Hashed based Collection type" do t = Puppet::Pops::Types::PStructType.new() tested_types = collection_types - [ Puppet::Pops::Types::PCollectionType, Puppet::Pops::Types::PStructType, Puppet::Pops::Types::PHashType] tested_types.each {|t2| t.should_not be_assignable_to(t2.new) } end it 'Struct is not assignable to any disjunct type' do tested_types = all_types - [ Puppet::Pops::Types::PAnyType, Puppet::Pops::Types::PDataType] - collection_types t = Puppet::Pops::Types::PStructType.new() tested_types.each {|t2| t.should_not be_assignable_to(t2.new) } end end context "for Callable, such that" do it "Callable is not assignable to any disjunct type" do t = Puppet::Pops::Types::PCallableType.new() tested_types = all_types - [ Puppet::Pops::Types::PCallableType, Puppet::Pops::Types::PAnyType] tested_types.each {|t2| t.should_not be_assignable_to(t2.new) } end end it 'should recognize mapped ruby types' do { Integer => Puppet::Pops::Types::PIntegerType.new, Fixnum => Puppet::Pops::Types::PIntegerType.new, Bignum => Puppet::Pops::Types::PIntegerType.new, Float => Puppet::Pops::Types::PFloatType.new, Numeric => Puppet::Pops::Types::PNumericType.new, NilClass => Puppet::Pops::Types::PNilType.new, TrueClass => Puppet::Pops::Types::PBooleanType.new, FalseClass => Puppet::Pops::Types::PBooleanType.new, String => Puppet::Pops::Types::PStringType.new, Regexp => Puppet::Pops::Types::PRegexpType.new, Regexp => Puppet::Pops::Types::PRegexpType.new, Array => Puppet::Pops::Types::TypeFactory.array_of_data(), Hash => Puppet::Pops::Types::TypeFactory.hash_of_data() }.each do |ruby_type, puppet_type | ruby_type.should be_assignable_to(puppet_type) end end context 'when dealing with integer ranges' do it 'should accept an equal range' do calculator.assignable?(range_t(2,5), range_t(2,5)).should == true end it 'should accept an equal reverse range' do calculator.assignable?(range_t(2,5), range_t(5,2)).should == true end it 'should accept a narrower range' do calculator.assignable?(range_t(2,10), range_t(3,5)).should == true end it 'should accept a narrower reverse range' do calculator.assignable?(range_t(2,10), range_t(5,3)).should == true end it 'should reject a wider range' do calculator.assignable?(range_t(3,5), range_t(2,10)).should == false end it 'should reject a wider reverse range' do calculator.assignable?(range_t(3,5), range_t(10,2)).should == false end it 'should reject a partially overlapping range' do calculator.assignable?(range_t(3,5), range_t(2,4)).should == false calculator.assignable?(range_t(3,5), range_t(4,6)).should == false end it 'should reject a partially overlapping reverse range' do calculator.assignable?(range_t(3,5), range_t(4,2)).should == false calculator.assignable?(range_t(3,5), range_t(6,4)).should == false end end context 'when dealing with patterns' do it 'should accept a string matching a pattern' do p_t = pattern_t('abc') p_s = string_t('XabcY') calculator.assignable?(p_t, p_s).should == true end it 'should accept a regexp matching a pattern' do p_t = pattern_t(/abc/) p_s = string_t('XabcY') calculator.assignable?(p_t, p_s).should == true end it 'should accept a pattern matching a pattern' do p_t = pattern_t(pattern_t('abc')) p_s = string_t('XabcY') calculator.assignable?(p_t, p_s).should == true end it 'should accept a regexp matching a pattern' do p_t = pattern_t(regexp_t('abc')) p_s = string_t('XabcY') calculator.assignable?(p_t, p_s).should == true end it 'should accept a string matching all patterns' do p_t = pattern_t('abc', 'ab', 'c') p_s = string_t('XabcY') calculator.assignable?(p_t, p_s).should == true end it 'should accept multiple strings if they all match any patterns' do p_t = pattern_t('X', 'Y', 'abc') p_s = string_t('Xa', 'aY', 'abc') calculator.assignable?(p_t, p_s).should == true end it 'should reject a string not matching any patterns' do p_t = pattern_t('abc', 'ab', 'c') p_s = string_t('XqqqY') calculator.assignable?(p_t, p_s).should == false end it 'should reject multiple strings if not all match any patterns' do p_t = pattern_t('abc', 'ab', 'c', 'q') p_s = string_t('X', 'Y', 'Z') calculator.assignable?(p_t, p_s).should == false end it 'should accept enum matching patterns as instanceof' do enum = enum_t('XS', 'S', 'M', 'L' 'XL', 'XXL') pattern = pattern_t('S', 'M', 'L') calculator.assignable?(pattern, enum).should == true end it 'pattern should accept a variant where all variants are acceptable' do pattern = pattern_t(/^\w+$/) calculator.assignable?(pattern, variant_t(string_t('a'), string_t('b'))).should == true end + it 'pattern representing all patterns should accept any pattern' do + calculator.assignable?(pattern_t(), pattern_t('a')).should == true + calculator.assignable?(pattern_t(), pattern_t()).should == true + end + + it 'pattern representing all patterns should accept any enum' do + calculator.assignable?(pattern_t(), enum_t('a')).should == true + calculator.assignable?(pattern_t(), enum_t()).should == true + end + + it 'pattern representing all patterns should accept any string' do + calculator.assignable?(pattern_t(), string_t('a')).should == true + calculator.assignable?(pattern_t(), string_t()).should == true + end + end context 'when dealing with enums' do it 'should accept a string with matching content' do calculator.assignable?(enum_t('a', 'b'), string_t('a')).should == true calculator.assignable?(enum_t('a', 'b'), string_t('b')).should == true calculator.assignable?(enum_t('a', 'b'), string_t('c')).should == false end it 'should accept an enum with matching enum' do calculator.assignable?(enum_t('a', 'b'), enum_t('a', 'b')).should == true calculator.assignable?(enum_t('a', 'b'), enum_t('a')).should == true calculator.assignable?(enum_t('a', 'b'), enum_t('c')).should == false end it 'non parameterized enum accepts any other enum but not the reverse' do calculator.assignable?(enum_t(), enum_t('a')).should == true calculator.assignable?(enum_t('a'), enum_t()).should == false end it 'enum should accept a variant where all variants are acceptable' do enum = enum_t('a', 'b') calculator.assignable?(enum, variant_t(string_t('a'), string_t('b'))).should == true end end context 'when dealing with string and enum combinations' do it 'should accept assigning any enum to unrestricted string' do calculator.assignable?(string_t(), enum_t('blue')).should == true calculator.assignable?(string_t(), enum_t('blue', 'red')).should == true end it 'should not accept assigning longer enum value to size restricted string' do calculator.assignable?(constrained_t(string_t(),2,2), enum_t('a','blue')).should == false end it 'should accept assigning any string to empty enum' do calculator.assignable?(enum_t(), string_t()).should == true end it 'should accept assigning empty enum to any string' do calculator.assignable?(string_t(), enum_t()).should == true end it 'should not accept assigning empty enum to size constrained string' do calculator.assignable?(constrained_t(string_t(),2,2), enum_t()).should == false end end + context 'when dealing with string/pattern/enum combinations' do + it 'any string is equal to any enum is equal to any pattern' do + calculator.assignable?(string_t(), enum_t()).should == true + calculator.assignable?(string_t(), pattern_t()).should == true + calculator.assignable?(enum_t(), string_t()).should == true + calculator.assignable?(enum_t(), pattern_t()).should == true + calculator.assignable?(pattern_t(), string_t()).should == true + calculator.assignable?(pattern_t(), enum_t()).should == true + end + end + context 'when dealing with tuples' do it 'matches empty tuples' do tuple1 = tuple_t() tuple2 = tuple_t() calculator.assignable?(tuple1, tuple2).should == true calculator.assignable?(tuple2, tuple1).should == true end it 'accepts an empty tuple as assignable to a tuple with a min size of 0' do tuple1 = tuple_t(Object) factory.constrain_size(tuple1, 0, :default) tuple2 = tuple_t() calculator.assignable?(tuple1, tuple2).should == true calculator.assignable?(tuple2, tuple1).should == false end it 'should accept matching tuples' do tuple1 = tuple_t(1,2) tuple2 = tuple_t(Integer,Integer) calculator.assignable?(tuple1, tuple2).should == true calculator.assignable?(tuple2, tuple1).should == true end it 'should accept matching tuples where one is more general than the other' do tuple1 = tuple_t(1,2) tuple2 = tuple_t(Numeric,Numeric) calculator.assignable?(tuple1, tuple2).should == false calculator.assignable?(tuple2, tuple1).should == true end it 'should accept ranged tuples' do tuple1 = tuple_t(1) factory.constrain_size(tuple1, 5, 5) tuple2 = tuple_t(Integer,Integer, Integer, Integer, Integer) calculator.assignable?(tuple1, tuple2).should == true calculator.assignable?(tuple2, tuple1).should == true end it 'should reject ranged tuples when ranges does not match' do tuple1 = tuple_t(1) factory.constrain_size(tuple1, 4, 5) tuple2 = tuple_t(Integer,Integer, Integer, Integer, Integer) calculator.assignable?(tuple1, tuple2).should == true calculator.assignable?(tuple2, tuple1).should == false end it 'should reject ranged tuples when ranges does not match (using infinite upper bound)' do tuple1 = tuple_t(1) factory.constrain_size(tuple1, 4, :default) tuple2 = tuple_t(Integer,Integer, Integer, Integer, Integer) calculator.assignable?(tuple1, tuple2).should == true calculator.assignable?(tuple2, tuple1).should == false end it 'should accept matching tuples with optional entries by repeating last' do tuple1 = tuple_t(1,2) factory.constrain_size(tuple1, 0, :default) tuple2 = tuple_t(Numeric,Numeric) factory.constrain_size(tuple2, 0, :default) calculator.assignable?(tuple1, tuple2).should == false calculator.assignable?(tuple2, tuple1).should == true end it 'should accept matching tuples with optional entries' do tuple1 = tuple_t(Integer, Integer, String) factory.constrain_size(tuple1, 1, 3) array2 = factory.constrain_size(array_t(Integer),2,2) calculator.assignable?(tuple1, array2).should == true factory.constrain_size(tuple1, 3, 3) calculator.assignable?(tuple1, array2).should == false end it 'should accept matching array' do tuple1 = tuple_t(1,2) array = array_t(Integer) factory.constrain_size(array, 2, 2) calculator.assignable?(tuple1, array).should == true calculator.assignable?(array, tuple1).should == true end it 'should accept empty array when tuple allows min of 0' do tuple1 = tuple_t(Integer) factory.constrain_size(tuple1, 0, 1) array = array_t(Integer) factory.constrain_size(array, 0, 0) calculator.assignable?(tuple1, array).should == true calculator.assignable?(array, tuple1).should == false end end context 'when dealing with structs' do it 'should accept matching structs' do struct1 = struct_t({'a'=>Integer, 'b'=>Integer}) struct2 = struct_t({'a'=>Integer, 'b'=>Integer}) calculator.assignable?(struct1, struct2).should == true calculator.assignable?(struct2, struct1).should == true end it 'should accept matching structs where one is more general than the other' do struct1 = struct_t({'a'=>Integer, 'b'=>Integer}) struct2 = struct_t({'a'=>Numeric, 'b'=>Numeric}) calculator.assignable?(struct1, struct2).should == false calculator.assignable?(struct2, struct1).should == true end it 'should accept matching hash' do struct1 = struct_t({'a'=>Integer, 'b'=>Integer}) non_empty_string = string_t() non_empty_string.size_type = range_t(1, nil) hsh = hash_t(non_empty_string, Integer) factory.constrain_size(hsh, 2, 2) calculator.assignable?(struct1, hsh).should == true calculator.assignable?(hsh, struct1).should == true end end it 'should recognize ruby type inheritance' do class Foo end class Bar < Foo end fooType = calculator.infer(Foo.new) barType = calculator.infer(Bar.new) calculator.assignable?(fooType, fooType).should == true calculator.assignable?(Foo, fooType).should == true calculator.assignable?(fooType, barType).should == true calculator.assignable?(Foo, barType).should == true calculator.assignable?(barType, fooType).should == false calculator.assignable?(Bar, fooType).should == false end it "should allow host class with same name" do hc1 = Puppet::Pops::Types::TypeFactory.host_class('the_name') hc2 = Puppet::Pops::Types::TypeFactory.host_class('the_name') calculator.assignable?(hc1, hc2).should == true end it "should allow host class with name assigned to hostclass without name" do hc1 = Puppet::Pops::Types::TypeFactory.host_class() hc2 = Puppet::Pops::Types::TypeFactory.host_class('the_name') calculator.assignable?(hc1, hc2).should == true end it "should reject host classes with different names" do hc1 = Puppet::Pops::Types::TypeFactory.host_class('the_name') hc2 = Puppet::Pops::Types::TypeFactory.host_class('another_name') calculator.assignable?(hc1, hc2).should == false end it "should reject host classes without name assigned to host class with name" do hc1 = Puppet::Pops::Types::TypeFactory.host_class('the_name') hc2 = Puppet::Pops::Types::TypeFactory.host_class() calculator.assignable?(hc1, hc2).should == false end it "should allow resource with same type_name and title" do r1 = Puppet::Pops::Types::TypeFactory.resource('file', 'foo') r2 = Puppet::Pops::Types::TypeFactory.resource('file', 'foo') calculator.assignable?(r1, r2).should == true end it "should allow more specific resource assignment" do r1 = Puppet::Pops::Types::TypeFactory.resource() r2 = Puppet::Pops::Types::TypeFactory.resource('file') calculator.assignable?(r1, r2).should == true r2 = Puppet::Pops::Types::TypeFactory.resource('file', '/tmp/foo') calculator.assignable?(r1, r2).should == true r1 = Puppet::Pops::Types::TypeFactory.resource('file') calculator.assignable?(r1, r2).should == true end it "should reject less specific resource assignment" do r1 = Puppet::Pops::Types::TypeFactory.resource('file', '/tmp/foo') r2 = Puppet::Pops::Types::TypeFactory.resource('file') calculator.assignable?(r1, r2).should == false r2 = Puppet::Pops::Types::TypeFactory.resource() calculator.assignable?(r1, r2).should == false end end context 'when testing if x is instance of type t' do include_context "types_setup" it 'should consider undef to be instance of Any, NilType, and optional' do calculator.instance?(Puppet::Pops::Types::PNilType.new(), nil).should == true calculator.instance?(Puppet::Pops::Types::PAnyType.new(), nil).should == true calculator.instance?(Puppet::Pops::Types::POptionalType.new(), nil).should == true end it 'all types should be (ruby) instance of PAnyType' do all_types.each do |t| t.new.is_a?(Puppet::Pops::Types::PAnyType).should == true end end it "should consider :undef to be instance of Runtime['ruby', 'Symbol]" do calculator.instance?(Puppet::Pops::Types::PRuntimeType.new(:runtime => :ruby, :runtime_type_name => 'Symbol'), :undef).should == true end + it "should consider :undef to be instance of an Optional type" do + calculator.instance?(Puppet::Pops::Types::POptionalType.new(), :undef).should == true + end + it 'should not consider undef to be an instance of any other type than Any, NilType and Data' do types_to_test = all_types - [ Puppet::Pops::Types::PAnyType, Puppet::Pops::Types::PNilType, Puppet::Pops::Types::PDataType, Puppet::Pops::Types::POptionalType, ] types_to_test.each {|t| calculator.instance?(t.new, nil).should == false } types_to_test.each {|t| calculator.instance?(t.new, :undef).should == false } end it 'should consider default to be instance of Default and Any' do calculator.instance?(Puppet::Pops::Types::PDefaultType.new(), :default).should == true calculator.instance?(Puppet::Pops::Types::PAnyType.new(), :default).should == true end it 'should not consider "default" to be an instance of anything but Default, and Any' do types_to_test = all_types - [ Puppet::Pops::Types::PAnyType, Puppet::Pops::Types::PDefaultType, ] types_to_test.each {|t| calculator.instance?(t.new, :default).should == false } end it 'should consider fixnum instanceof PIntegerType' do calculator.instance?(Puppet::Pops::Types::PIntegerType.new(), 1).should == true end it 'should consider fixnum instanceof Fixnum' do calculator.instance?(Fixnum, 1).should == true end it 'should consider integer in range' do range = range_t(0,10) calculator.instance?(range, 1).should == true calculator.instance?(range, 10).should == true calculator.instance?(range, -1).should == false calculator.instance?(range, 11).should == false end it 'should consider string in length range' do range = factory.constrain_size(string_t, 1,3) calculator.instance?(range, 'a').should == true calculator.instance?(range, 'abc').should == true calculator.instance?(range, '').should == false calculator.instance?(range, 'abcd').should == false end it 'should consider array in length range' do range = factory.constrain_size(array_t(integer_t), 1,3) calculator.instance?(range, [1]).should == true calculator.instance?(range, [1,2,3]).should == true calculator.instance?(range, []).should == false calculator.instance?(range, [1,2,3,4]).should == false end it 'should consider hash in length range' do range = factory.constrain_size(hash_t(integer_t, integer_t), 1,2) calculator.instance?(range, {1=>1}).should == true calculator.instance?(range, {1=>1, 2=>2}).should == true calculator.instance?(range, {}).should == false calculator.instance?(range, {1=>1, 2=>2, 3=>3}).should == false end it 'should consider collection in length range for array ' do range = factory.constrain_size(collection_t, 1,3) calculator.instance?(range, [1]).should == true calculator.instance?(range, [1,2,3]).should == true calculator.instance?(range, []).should == false calculator.instance?(range, [1,2,3,4]).should == false end it 'should consider collection in length range for hash' do range = factory.constrain_size(collection_t, 1,2) calculator.instance?(range, {1=>1}).should == true calculator.instance?(range, {1=>1, 2=>2}).should == true calculator.instance?(range, {}).should == false calculator.instance?(range, {1=>1, 2=>2, 3=>3}).should == false end it 'should consider string matching enum as instanceof' do enum = enum_t('XS', 'S', 'M', 'L', 'XL', '0') calculator.instance?(enum, 'XS').should == true calculator.instance?(enum, 'S').should == true calculator.instance?(enum, 'XXL').should == false calculator.instance?(enum, '').should == false calculator.instance?(enum, '0').should == true calculator.instance?(enum, 0).should == false end it 'should consider array[string] as instance of Array[Enum] when strings are instance of Enum' do enum = enum_t('XS', 'S', 'M', 'L', 'XL', '0') array = array_t(enum) calculator.instance?(array, ['XS', 'S', 'XL']).should == true calculator.instance?(array, ['XS', 'S', 'XXL']).should == false end it 'should consider array[mixed] as instance of Variant[mixed] when mixed types are listed in Variant' do enum = enum_t('XS', 'S', 'M', 'L', 'XL') sizes = range_t(30, 50) array = array_t(variant_t(enum, sizes)) calculator.instance?(array, ['XS', 'S', 30, 50]).should == true calculator.instance?(array, ['XS', 'S', 'XXL']).should == false calculator.instance?(array, ['XS', 'S', 29]).should == false end it 'should consider array[seq] as instance of Tuple[seq] when elements of seq are instance of' do tuple = tuple_t(Integer, String, Float) calculator.instance?(tuple, [1, 'a', 3.14]).should == true calculator.instance?(tuple, [1.2, 'a', 3.14]).should == false calculator.instance?(tuple, [1, 1, 3.14]).should == false calculator.instance?(tuple, [1, 'a', 1]).should == false end it 'should consider hash[cont] as instance of Struct[cont-t]' do struct = struct_t({'a'=>Integer, 'b'=>String, 'c'=>Float}) calculator.instance?(struct, {'a'=>1, 'b'=>'a', 'c'=>3.14}).should == true calculator.instance?(struct, {'a'=>1.2, 'b'=>'a', 'c'=>3.14}).should == false calculator.instance?(struct, {'a'=>1, 'b'=>1, 'c'=>3.14}).should == false calculator.instance?(struct, {'a'=>1, 'b'=>'a', 'c'=>1}).should == false end context 'and t is Data' do it 'undef should be considered instance of Data' do calculator.instance?(data_t, nil).should == true end it 'other symbols should not be considered instance of Data' do calculator.instance?(data_t, :love).should == false end it 'an empty array should be considered instance of Data' do calculator.instance?(data_t, []).should == true end it 'an empty hash should be considered instance of Data' do calculator.instance?(data_t, {}).should == true end it 'a hash with nil/undef data should be considered instance of Data' do calculator.instance?(data_t, {'a' => nil}).should == true end it 'a hash with nil/default key should not considered instance of Data' do calculator.instance?(data_t, {nil => 10}).should == false calculator.instance?(data_t, {:default => 10}).should == false end it 'an array with nil entries should be considered instance of Data' do calculator.instance?(data_t, [nil]).should == true end it 'an array with nil + data entries should be considered instance of Data' do calculator.instance?(data_t, [1, nil, 'a']).should == true end end context "and t is something Callable" do it 'a Closure should be considered a Callable' do factory = Puppet::Pops::Model::Factory params = [factory.PARAM('a')] the_block = factory.LAMBDA(params,factory.literal(42)) the_closure = Puppet::Pops::Evaluator::Closure.new(:fake_evaluator, the_block, :fake_scope) expect(calculator.instance?(all_callables_t, the_closure)).to be_true expect(calculator.instance?(callable_t(object_t), the_closure)).to be_true expect(calculator.instance?(callable_t(object_t, object_t), the_closure)).to be_false end it 'a Function instance should be considered a Callable' do fc = Puppet::Functions.create_function(:foo) do dispatch :foo do param 'String', 'a' end def foo(a) a end end f = fc.new(:closure_scope, :loader) # Any callable expect(calculator.instance?(all_callables_t, f)).to be_true # Callable[String] expect(calculator.instance?(callable_t(String), f)).to be_true end end end context 'when converting a ruby class' do it 'should yield \'PIntegerType\' for Integer, Fixnum, and Bignum' do [Integer,Fixnum,Bignum].each do |c| calculator.type(c).class.should == Puppet::Pops::Types::PIntegerType end end it 'should yield \'PFloatType\' for Float' do calculator.type(Float).class.should == Puppet::Pops::Types::PFloatType end it 'should yield \'PBooleanType\' for FalseClass and TrueClass' do [FalseClass,TrueClass].each do |c| calculator.type(c).class.should == Puppet::Pops::Types::PBooleanType end end it 'should yield \'PNilType\' for NilClass' do calculator.type(NilClass).class.should == Puppet::Pops::Types::PNilType end it 'should yield \'PStringType\' for String' do calculator.type(String).class.should == Puppet::Pops::Types::PStringType end it 'should yield \'PRegexpType\' for Regexp' do calculator.type(Regexp).class.should == Puppet::Pops::Types::PRegexpType end it 'should yield \'PArrayType[PDataType]\' for Array' do t = calculator.type(Array) t.class.should == Puppet::Pops::Types::PArrayType t.element_type.class.should == Puppet::Pops::Types::PDataType end it 'should yield \'PHashType[PScalarType,PDataType]\' for Hash' do t = calculator.type(Hash) t.class.should == Puppet::Pops::Types::PHashType t.key_type.class.should == Puppet::Pops::Types::PScalarType t.element_type.class.should == Puppet::Pops::Types::PDataType end end context 'when representing the type as string' do it 'should yield \'Type\' for PType' do calculator.string(Puppet::Pops::Types::PType.new()).should == 'Type' end it 'should yield \'Object\' for PAnyType' do calculator.string(Puppet::Pops::Types::PAnyType.new()).should == 'Any' end it 'should yield \'Scalar\' for PScalarType' do calculator.string(Puppet::Pops::Types::PScalarType.new()).should == 'Scalar' end it 'should yield \'Boolean\' for PBooleanType' do calculator.string(Puppet::Pops::Types::PBooleanType.new()).should == 'Boolean' end it 'should yield \'Data\' for PDataType' do calculator.string(Puppet::Pops::Types::PDataType.new()).should == 'Data' end it 'should yield \'Numeric\' for PNumericType' do calculator.string(Puppet::Pops::Types::PNumericType.new()).should == 'Numeric' end it 'should yield \'Integer\' and from/to for PIntegerType' do int_T = Puppet::Pops::Types::PIntegerType calculator.string(int_T.new()).should == 'Integer' int = int_T.new() int.from = 1 int.to = 1 calculator.string(int).should == 'Integer[1, 1]' int = int_T.new() int.from = 1 int.to = 2 calculator.string(int).should == 'Integer[1, 2]' int = int_T.new() int.from = nil int.to = 2 calculator.string(int).should == 'Integer[default, 2]' int = int_T.new() int.from = 2 int.to = nil calculator.string(int).should == 'Integer[2, default]' end it 'should yield \'Float\' for PFloatType' do calculator.string(Puppet::Pops::Types::PFloatType.new()).should == 'Float' end it 'should yield \'Regexp\' for PRegexpType' do calculator.string(Puppet::Pops::Types::PRegexpType.new()).should == 'Regexp' end it 'should yield \'Regexp[/pat/]\' for parameterized PRegexpType' do t = Puppet::Pops::Types::PRegexpType.new() t.pattern = ('a/b') calculator.string(Puppet::Pops::Types::PRegexpType.new()).should == 'Regexp' end it 'should yield \'String\' for PStringType' do calculator.string(Puppet::Pops::Types::PStringType.new()).should == 'String' end it 'should yield \'String\' for PStringType with multiple values' do calculator.string(string_t('a', 'b', 'c')).should == 'String' end it 'should yield \'String\' and from/to for PStringType' do string_T = Puppet::Pops::Types::PStringType calculator.string(factory.constrain_size(string_T.new(), 1,1)).should == 'String[1, 1]' calculator.string(factory.constrain_size(string_T.new(), 1,2)).should == 'String[1, 2]' calculator.string(factory.constrain_size(string_T.new(), :default, 2)).should == 'String[default, 2]' calculator.string(factory.constrain_size(string_T.new(), 2, :default)).should == 'String[2, default]' end it 'should yield \'Array[Integer]\' for PArrayType[PIntegerType]' do t = Puppet::Pops::Types::PArrayType.new() t.element_type = Puppet::Pops::Types::PIntegerType.new() calculator.string(t).should == 'Array[Integer]' end it 'should yield \'Collection\' and from/to for PCollectionType' do col = collection_t() calculator.string(factory.constrain_size(col.copy, 1,1)).should == 'Collection[1, 1]' calculator.string(factory.constrain_size(col.copy, 1,2)).should == 'Collection[1, 2]' calculator.string(factory.constrain_size(col.copy, :default, 2)).should == 'Collection[default, 2]' calculator.string(factory.constrain_size(col.copy, 2, :default)).should == 'Collection[2, default]' end it 'should yield \'Array\' and from/to for PArrayType' do arr = array_t(string_t) calculator.string(factory.constrain_size(arr.copy, 1,1)).should == 'Array[String, 1, 1]' calculator.string(factory.constrain_size(arr.copy, 1,2)).should == 'Array[String, 1, 2]' calculator.string(factory.constrain_size(arr.copy, :default, 2)).should == 'Array[String, default, 2]' calculator.string(factory.constrain_size(arr.copy, 2, :default)).should == 'Array[String, 2, default]' end it 'should yield \'Tuple[Integer]\' for PTupleType[PIntegerType]' do t = Puppet::Pops::Types::PTupleType.new() t.addTypes(Puppet::Pops::Types::PIntegerType.new()) calculator.string(t).should == 'Tuple[Integer]' end it 'should yield \'Tuple[T, T,..]\' for PTupleType[T, T, ...]' do t = Puppet::Pops::Types::PTupleType.new() t.addTypes(Puppet::Pops::Types::PIntegerType.new()) t.addTypes(Puppet::Pops::Types::PIntegerType.new()) t.addTypes(Puppet::Pops::Types::PStringType.new()) calculator.string(t).should == 'Tuple[Integer, Integer, String]' end it 'should yield \'Tuple\' and from/to for PTupleType' do tuple_t = tuple_t(string_t) calculator.string(factory.constrain_size(tuple_t.copy, 1,1)).should == 'Tuple[String, 1, 1]' calculator.string(factory.constrain_size(tuple_t.copy, 1,2)).should == 'Tuple[String, 1, 2]' calculator.string(factory.constrain_size(tuple_t.copy, :default, 2)).should == 'Tuple[String, default, 2]' calculator.string(factory.constrain_size(tuple_t.copy, 2, :default)).should == 'Tuple[String, 2, default]' end it 'should yield \'Struct\' and details for PStructType' do struct_t = struct_t({'a'=>Integer, 'b'=>String}) s = calculator.string(struct_t) # Ruby 1.8.7 - noone likes you... (s == "Struct[{'a'=>Integer, 'b'=>String}]" || s == "Struct[{'b'=>String, 'a'=>Integer}]").should == true struct_t = struct_t({}) calculator.string(struct_t).should == "Struct" end it 'should yield \'Hash[String, Integer]\' for PHashType[PStringType, PIntegerType]' do t = Puppet::Pops::Types::PHashType.new() t.key_type = Puppet::Pops::Types::PStringType.new() t.element_type = Puppet::Pops::Types::PIntegerType.new() calculator.string(t).should == 'Hash[String, Integer]' end it 'should yield \'Hash\' and from/to for PHashType' do hsh = hash_t(string_t, string_t) calculator.string(factory.constrain_size(hsh.copy, 1,1)).should == 'Hash[String, String, 1, 1]' calculator.string(factory.constrain_size(hsh.copy, 1,2)).should == 'Hash[String, String, 1, 2]' calculator.string(factory.constrain_size(hsh.copy, :default, 2)).should == 'Hash[String, String, default, 2]' calculator.string(factory.constrain_size(hsh.copy, 2, :default)).should == 'Hash[String, String, 2, default]' end it "should yield 'Class' for a PHostClassType" do t = Puppet::Pops::Types::PHostClassType.new() calculator.string(t).should == 'Class' end it "should yield 'Class[x]' for a PHostClassType[x]" do t = Puppet::Pops::Types::PHostClassType.new() t.class_name = 'x' calculator.string(t).should == 'Class[x]' end it "should yield 'Resource' for a PResourceType" do t = Puppet::Pops::Types::PResourceType.new() calculator.string(t).should == 'Resource' end it 'should yield \'File\' for a PResourceType[\'File\']' do t = Puppet::Pops::Types::PResourceType.new() t.type_name = 'File' calculator.string(t).should == 'File' end it "should yield 'File['/tmp/foo']' for a PResourceType['File', '/tmp/foo']" do t = Puppet::Pops::Types::PResourceType.new() t.type_name = 'File' t.title = '/tmp/foo' calculator.string(t).should == "File['/tmp/foo']" end it "should yield 'Enum[s,...]' for a PEnumType[s,...]" do t = enum_t('a', 'b', 'c') calculator.string(t).should == "Enum['a', 'b', 'c']" end it "should yield 'Pattern[/pat/,...]' for a PPatternType['pat',...]" do t = pattern_t('a') t2 = pattern_t('a', 'b', 'c') calculator.string(t).should == "Pattern[/a/]" calculator.string(t2).should == "Pattern[/a/, /b/, /c/]" end it "should escape special characters in the string for a PPatternType['pat',...]" do t = pattern_t('a/b') calculator.string(t).should == "Pattern[/a\\/b/]" end it "should yield 'Variant[t1,t2,...]' for a PVariantType[t1, t2,...]" do t1 = string_t() t2 = integer_t() t3 = pattern_t('a') t = variant_t(t1, t2, t3) calculator.string(t).should == "Variant[String, Integer, Pattern[/a/]]" end it "should yield 'Callable' for generic callable" do expect(calculator.string(all_callables_t)).to eql("Callable") end it "should yield 'Callable[0,0]' for callable without params" do expect(calculator.string(callable_t)).to eql("Callable[0, 0]") end it "should yield 'Callable[t,t]' for callable with typed parameters" do expect(calculator.string(callable_t(String, Integer))).to eql("Callable[String, Integer]") end it "should yield 'Callable[t,min,max]' for callable with size constraint (infinite max)" do expect(calculator.string(callable_t(String, 0))).to eql("Callable[String, 0, default]") end it "should yield 'Callable[t,min,max]' for callable with size constraint (capped max)" do expect(calculator.string(callable_t(String, 0, 3))).to eql("Callable[String, 0, 3]") end it "should yield 'Callable[min,max]' callable with size > 0" do expect(calculator.string(callable_t(0, 0))).to eql("Callable[0, 0]") expect(calculator.string(callable_t(0, 1))).to eql("Callable[0, 1]") expect(calculator.string(callable_t(0, :default))).to eql("Callable[0, default]") end it "should yield 'Callable[Callable]' for callable with block" do expect(calculator.string(callable_t(all_callables_t))).to eql("Callable[0, 0, Callable]") expect(calculator.string(callable_t(string_t, all_callables_t))).to eql("Callable[String, Callable]") expect(calculator.string(callable_t(string_t, 1,1, all_callables_t))).to eql("Callable[String, 1, 1, Callable]") end it "should yield Unit for a Unit type" do expect(calculator.string(unit_t)).to eql('Unit') end end context 'when processing meta type' do it 'should infer PType as the type of all other types' do ptype = Puppet::Pops::Types::PType calculator.infer(Puppet::Pops::Types::PNilType.new() ).is_a?(ptype).should() == true calculator.infer(Puppet::Pops::Types::PDataType.new() ).is_a?(ptype).should() == true calculator.infer(Puppet::Pops::Types::PScalarType.new() ).is_a?(ptype).should() == true calculator.infer(Puppet::Pops::Types::PStringType.new() ).is_a?(ptype).should() == true calculator.infer(Puppet::Pops::Types::PNumericType.new() ).is_a?(ptype).should() == true calculator.infer(Puppet::Pops::Types::PIntegerType.new() ).is_a?(ptype).should() == true calculator.infer(Puppet::Pops::Types::PFloatType.new() ).is_a?(ptype).should() == true calculator.infer(Puppet::Pops::Types::PRegexpType.new() ).is_a?(ptype).should() == true calculator.infer(Puppet::Pops::Types::PBooleanType.new() ).is_a?(ptype).should() == true calculator.infer(Puppet::Pops::Types::PCollectionType.new()).is_a?(ptype).should() == true calculator.infer(Puppet::Pops::Types::PArrayType.new() ).is_a?(ptype).should() == true calculator.infer(Puppet::Pops::Types::PHashType.new() ).is_a?(ptype).should() == true calculator.infer(Puppet::Pops::Types::PRuntimeType.new() ).is_a?(ptype).should() == true calculator.infer(Puppet::Pops::Types::PHostClassType.new() ).is_a?(ptype).should() == true calculator.infer(Puppet::Pops::Types::PResourceType.new() ).is_a?(ptype).should() == true calculator.infer(Puppet::Pops::Types::PEnumType.new() ).is_a?(ptype).should() == true calculator.infer(Puppet::Pops::Types::PPatternType.new() ).is_a?(ptype).should() == true calculator.infer(Puppet::Pops::Types::PVariantType.new() ).is_a?(ptype).should() == true calculator.infer(Puppet::Pops::Types::PTupleType.new() ).is_a?(ptype).should() == true calculator.infer(Puppet::Pops::Types::POptionalType.new() ).is_a?(ptype).should() == true calculator.infer(Puppet::Pops::Types::PCallableType.new() ).is_a?(ptype).should() == true end it 'should infer PType as the type of all other types' do ptype = Puppet::Pops::Types::PType calculator.string(calculator.infer(Puppet::Pops::Types::PNilType.new() )).should == "Type[Undef]" calculator.string(calculator.infer(Puppet::Pops::Types::PDataType.new() )).should == "Type[Data]" calculator.string(calculator.infer(Puppet::Pops::Types::PScalarType.new() )).should == "Type[Scalar]" calculator.string(calculator.infer(Puppet::Pops::Types::PStringType.new() )).should == "Type[String]" calculator.string(calculator.infer(Puppet::Pops::Types::PNumericType.new() )).should == "Type[Numeric]" calculator.string(calculator.infer(Puppet::Pops::Types::PIntegerType.new() )).should == "Type[Integer]" calculator.string(calculator.infer(Puppet::Pops::Types::PFloatType.new() )).should == "Type[Float]" calculator.string(calculator.infer(Puppet::Pops::Types::PRegexpType.new() )).should == "Type[Regexp]" calculator.string(calculator.infer(Puppet::Pops::Types::PBooleanType.new() )).should == "Type[Boolean]" calculator.string(calculator.infer(Puppet::Pops::Types::PCollectionType.new())).should == "Type[Collection]" calculator.string(calculator.infer(Puppet::Pops::Types::PArrayType.new() )).should == "Type[Array[?]]" calculator.string(calculator.infer(Puppet::Pops::Types::PHashType.new() )).should == "Type[Hash[?, ?]]" calculator.string(calculator.infer(Puppet::Pops::Types::PRuntimeType.new() )).should == "Type[Runtime[?, ?]]" calculator.string(calculator.infer(Puppet::Pops::Types::PHostClassType.new() )).should == "Type[Class]" calculator.string(calculator.infer(Puppet::Pops::Types::PResourceType.new() )).should == "Type[Resource]" calculator.string(calculator.infer(Puppet::Pops::Types::PEnumType.new() )).should == "Type[Enum]" calculator.string(calculator.infer(Puppet::Pops::Types::PVariantType.new() )).should == "Type[Variant]" calculator.string(calculator.infer(Puppet::Pops::Types::PPatternType.new() )).should == "Type[Pattern]" calculator.string(calculator.infer(Puppet::Pops::Types::PTupleType.new() )).should == "Type[Tuple]" calculator.string(calculator.infer(Puppet::Pops::Types::POptionalType.new() )).should == "Type[Optional]" calculator.string(calculator.infer(Puppet::Pops::Types::PCallableType.new() )).should == "Type[Callable]" calculator.infer(Puppet::Pops::Types::PResourceType.new(:type_name => 'foo::fee::fum')).to_s.should == "Type[Foo::Fee::Fum]" calculator.string(calculator.infer(Puppet::Pops::Types::PResourceType.new(:type_name => 'foo::fee::fum'))).should == "Type[Foo::Fee::Fum]" calculator.infer(Puppet::Pops::Types::PResourceType.new(:type_name => 'Foo::Fee::Fum')).to_s.should == "Type[Foo::Fee::Fum]" end it "computes the common type of PType's type parameter" do int_t = Puppet::Pops::Types::PIntegerType.new() string_t = Puppet::Pops::Types::PStringType.new() calculator.string(calculator.infer([int_t])).should == "Array[Type[Integer], 1, 1]" calculator.string(calculator.infer([int_t, string_t])).should == "Array[Type[Scalar], 2, 2]" end it 'should infer PType as the type of ruby classes' do class Foo end [Object, Numeric, Integer, Fixnum, Bignum, Float, String, Regexp, Array, Hash, Foo].each do |c| calculator.infer(c).is_a?(Puppet::Pops::Types::PType).should() == true end end it 'should infer PType as the type of PType (meta regression short-circuit)' do calculator.infer(Puppet::Pops::Types::PType.new()).is_a?(Puppet::Pops::Types::PType).should() == true end it 'computes instance? to be true if parameterized and type match' do int_t = Puppet::Pops::Types::PIntegerType.new() type_t = Puppet::Pops::Types::TypeFactory.type_type(int_t) type_type_t = Puppet::Pops::Types::TypeFactory.type_type(type_t) calculator.instance?(type_type_t, type_t).should == true end it 'computes instance? to be false if parameterized and type do not match' do int_t = Puppet::Pops::Types::PIntegerType.new() string_t = Puppet::Pops::Types::PStringType.new() type_t = Puppet::Pops::Types::TypeFactory.type_type(int_t) type_t2 = Puppet::Pops::Types::TypeFactory.type_type(string_t) type_type_t = Puppet::Pops::Types::TypeFactory.type_type(type_t) # i.e. Type[Integer] =~ Type[Type[Integer]] # false calculator.instance?(type_type_t, type_t2).should == false end it 'computes instance? to be true if unparameterized and matched against a type[?]' do int_t = Puppet::Pops::Types::PIntegerType.new() type_t = Puppet::Pops::Types::TypeFactory.type_type(int_t) calculator.instance?(Puppet::Pops::Types::PType.new, type_t).should == true end end context "when asking for an enumerable " do it "should produce an enumerable for an Integer range that is not infinite" do t = Puppet::Pops::Types::PIntegerType.new() t.from = 1 t.to = 10 calculator.enumerable(t).respond_to?(:each).should == true end it "should not produce an enumerable for an Integer range that has an infinite side" do t = Puppet::Pops::Types::PIntegerType.new() t.from = nil t.to = 10 calculator.enumerable(t).should == nil t = Puppet::Pops::Types::PIntegerType.new() t.from = 1 t.to = nil calculator.enumerable(t).should == nil end it "all but Integer range are not enumerable" do [Object, Numeric, Float, String, Regexp, Array, Hash].each do |t| calculator.enumerable(calculator.type(t)).should == nil end end end context "when dealing with different types of inference" do it "an instance specific inference is produced by infer" do calculator.infer(['a','b']).element_type.values.should == ['a', 'b'] end it "a generic inference is produced using infer_generic" do calculator.infer_generic(['a','b']).element_type.values.should == [] end it "a generic result is created by generalize! given an instance specific result for an Array" do generic = calculator.infer(['a','b']) generic.element_type.values.should == ['a', 'b'] calculator.generalize!(generic) generic.element_type.values.should == [] end it "a generic result is created by generalize! given an instance specific result for a Hash" do generic = calculator.infer({'a' =>1,'b' => 2}) generic.key_type.values.sort.should == ['a', 'b'] generic.element_type.from.should == 1 generic.element_type.to.should == 2 calculator.generalize!(generic) generic.key_type.values.should == [] generic.element_type.from.should == nil generic.element_type.to.should == nil end it "does not reduce by combining types when using infer_set" do element_type = calculator.infer(['a','b',1,2]).element_type element_type.class.should == Puppet::Pops::Types::PScalarType inferred_type = calculator.infer_set(['a','b',1,2]) inferred_type.class.should == Puppet::Pops::Types::PTupleType element_types = inferred_type.types element_types[0].class.should == Puppet::Pops::Types::PStringType element_types[1].class.should == Puppet::Pops::Types::PStringType element_types[2].class.should == Puppet::Pops::Types::PIntegerType element_types[3].class.should == Puppet::Pops::Types::PIntegerType end it "does not reduce by combining types when using infer_set and values are undef" do element_type = calculator.infer(['a',nil]).element_type element_type.class.should == Puppet::Pops::Types::PStringType inferred_type = calculator.infer_set(['a',nil]) inferred_type.class.should == Puppet::Pops::Types::PTupleType element_types = inferred_type.types element_types[0].class.should == Puppet::Pops::Types::PStringType element_types[1].class.should == Puppet::Pops::Types::PNilType end end context 'when determening callability' do context 'and given is exact' do it 'with callable' do required = callable_t(string_t) given = callable_t(string_t) calculator.callable?(required, given).should == true end it 'with args tuple' do required = callable_t(string_t) given = tuple_t(string_t) calculator.callable?(required, given).should == true end it 'with args tuple having a block' do required = callable_t(string_t, callable_t(string_t)) given = tuple_t(string_t, callable_t(string_t)) calculator.callable?(required, given).should == true end it 'with args array' do required = callable_t(string_t) given = array_t(string_t) factory.constrain_size(given, 1, 1) calculator.callable?(required, given).should == true end end context 'and given is more generic' do it 'with callable' do required = callable_t(string_t) given = callable_t(object_t) calculator.callable?(required, given).should == true end it 'with args tuple' do required = callable_t(string_t) given = tuple_t(object_t) calculator.callable?(required, given).should == false end it 'with args tuple having a block' do required = callable_t(string_t, callable_t(string_t)) given = tuple_t(string_t, callable_t(object_t)) calculator.callable?(required, given).should == true end it 'with args tuple having a block with captures rest' do required = callable_t(string_t, callable_t(string_t)) given = tuple_t(string_t, callable_t(object_t, 0, :default)) calculator.callable?(required, given).should == true end end context 'and given is more specific' do it 'with callable' do required = callable_t(object_t) given = callable_t(string_t) calculator.callable?(required, given).should == false end it 'with args tuple' do required = callable_t(object_t) given = tuple_t(string_t) calculator.callable?(required, given).should == true end it 'with args tuple having a block' do required = callable_t(string_t, callable_t(object_t)) given = tuple_t(string_t, callable_t(string_t)) calculator.callable?(required, given).should == false end it 'with args tuple having a block with captures rest' do required = callable_t(string_t, callable_t(object_t)) given = tuple_t(string_t, callable_t(string_t, 0, :default)) calculator.callable?(required, given).should == false end end end matcher :be_assignable_to do |type| calc = Puppet::Pops::Types::TypeCalculator.new match do |actual| calc.assignable?(type, actual) end failure_message_for_should do |actual| "#{calc.string(actual)} should be assignable to #{calc.string(type)}" end failure_message_for_should_not do |actual| "#{calc.string(actual)} is assignable to #{calc.string(type)} when it should not" end end end diff --git a/spec/unit/provider/ssh_authorized_key/parsed_spec.rb b/spec/unit/provider/ssh_authorized_key/parsed_spec.rb index 2e88c57df..78abec901 100755 --- a/spec/unit/provider/ssh_authorized_key/parsed_spec.rb +++ b/spec/unit/provider/ssh_authorized_key/parsed_spec.rb @@ -1,261 +1,276 @@ #! /usr/bin/env ruby require 'spec_helper' require 'shared_behaviours/all_parsedfile_providers' require 'puppet_spec/files' provider_class = Puppet::Type.type(:ssh_authorized_key).provider(:parsed) describe provider_class, :unless => Puppet.features.microsoft_windows? do include PuppetSpec::Files before :each do @keyfile = tmpfile('authorized_keys') @provider_class = provider_class @provider_class.initvars @provider_class.any_instance.stubs(:target).returns @keyfile @user = 'random_bob' Puppet::Util.stubs(:uid).with(@user).returns 12345 end def mkkey(args) args[:target] = @keyfile args[:user] = @user resource = Puppet::Type.type(:ssh_authorized_key).new(args) key = @provider_class.new(resource) args.each do |p,v| key.send(p.to_s + "=", v) end key end def genkey(key) @provider_class.stubs(:filetype).returns(Puppet::Util::FileType::FileTypeRam) File.stubs(:chown) File.stubs(:chmod) Puppet::Util::SUIDManager.stubs(:asuser).yields key.flush @provider_class.target_object(@keyfile).read end it_should_behave_like "all parsedfile providers", provider_class it "should be able to generate a basic authorized_keys file" do key = mkkey(:name => "Just_Testing", :key => "AAAAfsfddsjldjgksdflgkjsfdlgkj", :type => "ssh-dss", :ensure => :present, :options => [:absent] ) genkey(key).should == "ssh-dss AAAAfsfddsjldjgksdflgkjsfdlgkj Just_Testing\n" end it "should be able to generate an authorized_keys file with options" do key = mkkey(:name => "root@localhost", :key => "AAAAfsfddsjldjgksdflgkjsfdlgkj", :type => "ssh-rsa", :ensure => :present, :options => ['from="192.168.1.1"', "no-pty", "no-X11-forwarding"] ) genkey(key).should == "from=\"192.168.1.1\",no-pty,no-X11-forwarding ssh-rsa AAAAfsfddsjldjgksdflgkjsfdlgkj root@localhost\n" end it "should parse comments" do result = [{ :record_type => :comment, :line => "# hello" }] @provider_class.parse("# hello\n").should == result end it "should parse comments with leading whitespace" do result = [{ :record_type => :comment, :line => " # hello" }] @provider_class.parse(" # hello\n").should == result end it "should skip over lines with only whitespace" do result = [{ :record_type => :comment, :line => "#before" }, { :record_type => :blank, :line => " " }, { :record_type => :comment, :line => "#after" }] @provider_class.parse("#before\n \n#after\n").should == result end it "should skip over completely empty lines" do result = [{ :record_type => :comment, :line => "#before"}, { :record_type => :blank, :line => ""}, { :record_type => :comment, :line => "#after"}] @provider_class.parse("#before\n\n#after\n").should == result end it "should be able to parse name if it includes whitespace" do @provider_class.parse_line('ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC7pHZ1XRj3tXbFpPFhMGU1bVwz7jr13zt/wuE+pVIJA8GlmHYuYtIxHPfDHlkixdwLachCpSQUL9NbYkkRFRn9m6PZ7125ohE4E4m96QS6SGSQowTiRn4Lzd9LV38g93EMHjPmEkdSq7MY4uJEd6DUYsLvaDYdIgBiLBIWPA3OrQ== fancy user')[:name].should == 'fancy user' @provider_class.parse_line('from="host1.reductlivelabs.com,host.reductivelabs.com",command="/usr/local/bin/run",ssh-pty ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC7pHZ1XRj3tXbFpPFhMGU1bVwz7jr13zt/wuE+pVIJA8GlmHYuYtIxHPfDHlkixdwLachCpSQUL9NbYkkRFRn9m6PZ7125ohE4E4m96QS6SGSQowTiRn4Lzd9LV38g93EMHjPmEkdSq7MY4uJEd6DUYsLvaDYdIgBiLBIWPA3OrQ== fancy user')[:name].should == 'fancy user' end it "should be able to parse options containing commas via its parse_options method" do options = %w{from="host1.reductlivelabs.com,host.reductivelabs.com" command="/usr/local/bin/run" ssh-pty} optionstr = options.join(", ") @provider_class.parse_options(optionstr).should == options end it "should parse quoted options" do line = 'command="/usr/local/bin/mybin \"$SSH_ORIGINAL_COMMAND\"" ssh-rsa xxx mykey' @provider_class.parse(line)[0][:options][0].should == 'command="/usr/local/bin/mybin \"$SSH_ORIGINAL_COMMAND\""' end it "should use '' as name for entries that lack a comment" do line = "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAut8aOSxenjOqF527dlsdHWV4MNoAsX14l9M297+SQXaQ5Z3BedIxZaoQthkDALlV/25A1COELrg9J2MqJNQc8Xe9XQOIkBQWWinUlD/BXwoOTWEy8C8zSZPHZ3getMMNhGTBO+q/O+qiJx3y5cA4MTbw2zSxukfWC87qWwcZ64UUlegIM056vPsdZWFclS9hsROVEa57YUMrehQ1EGxT4Z5j6zIopufGFiAPjZigq/vqgcAqhAKP6yu4/gwO6S9tatBeEjZ8fafvj1pmvvIplZeMr96gHE7xS3pEEQqnB3nd4RY7AF6j9kFixnsytAUO7STPh/M3pLiVQBN89TvWPQ==" @provider_class.parse(line)[0][:name].should == "" end { # ssh-keygen -t dsa -b 1024 'ssh-dss' => 'AAAAB3NzaC1kc3MAAACBANGTefWMXS780qLMMgysq3GNMKzg55LXZODif6Tqv1vtTh4Wuk3J5X5u644jTyNdAIn1RiBI9MnwnZMZ6nXKvucMcMQWMibYS9W2MhkRj3oqsLWMMsdGXJL18SWM5A6oC3oIRC4JHJZtkm0OctR2trKxmX+MGhdCd+Xpsh9CNK8XAAAAFQD4olFiwv+QQUFdaZbWUy1CLEG9xQAAAIByCkXKgoriZF8bQ0OX1sKuR69M/6n5ngmQGVBKB7BQkpUjbK/OggB6iJgst5utKkDcaqYRnrTYG9q3jJ/flv7yYePuoSreS0nCMMx9gpEYuq+7Sljg9IecmN/IHrNd9qdYoASy5iuROQMvEZM7KFHA8vBv0tWdBOsp4hZKyiL1DAAAAIEAjkZlOps9L+cD/MTzxDj7toYYypdLOvjlcPBaglkPZoFZ0MAKTI0zXlVX1cWAnkd0Yfo4EpP+6XAjlZkod+QXKXM4Tb4PnR34ASMeU6sEjM61Na24S7JD3gpPKataFU/oH3hzXsBdK2ttKYmoqvf61h32IA/3Z5PjCCD9pPLPpAY', # ssh-keygen -t rsa -b 2048 'ssh-rsa' => 'AAAAB3NzaC1yc2EAAAADAQABAAABAQDYtEaWa1mlxaAh9vtiz6RCVKDiJHDY15nsqqWU7F7A1+U1498+sWDyRDkZ8vXWQpzyOMBzBSHIxhsprlKhkjomy8BuJP+bHDBIKx4zgSFDrklrPIf467Iuug8J0qqDLxO4rOOjeAiLEyC0t2ZGnsTEea+rmat0bJ2cv3g5L4gH/OFz2pI4ZLp1HGN83ipl5UH8CjXQKwo3Db1E3WJCqKgszVX0Z4/qjnBRxFMoqky/1mGb/mX1eoT9JyQ8OhU9uENZOShkksSpgUqjlrjpj0Yd14hBlnE3M18pE4ivxjzectA/XRKNZaxOL1YREtU8sXusAwmlEY4aJ64aR0JrXfgx', # ssh-keygen -t ecdsa -b 256 'ecdsa-sha2-nistp256' => 'AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBBO5PfBf0c2jAuqD+Lj3j+SuXOXNT2uqESLVOn5jVQfEF9GzllOw+CMOpUvV1CiOOn+F1ET15vcsfmD7z05WUTA=', # ssh-keygen -t ecdsa -b 384 'ecdsa-sha2-nistp384' => 'AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBJIfxNoVK4FX3RuMlkHOwwxXwAh6Fqx5uAp4ftXrJ+64qYuIzb+/zSAkJV698Sre1b1lb0G4LyDdVAvXwaYK9kN25vy8umV3WdfZeHKXJGCcrplMCbbOERWARlpiPNEblg==', # ssh-keygen -t ecdsa -b 521 'ecdsa-sha2-nistp521' => 'AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBADLK+u12xwB0JOwpmaxYXv8KnPK4p+SE2405qoo+vpAQ569fMwPMgKzltd770amdeuFogw/MJu17PN9LDdrD3o0uwHMjWee6TpHQDkuEetaxiou6K0WAzgbxx9QsY0MsJgXf1BuMLqdK+xT183wOSXwwumv99G7T32dOJZ5tYrH0y4XMw==', # ssh-keygen -t ed25519 'ssh-ed25519' => 'AAAAC3NzaC1lZDI1NTE5AAAAIBWvu7D1KHBPaNXQcEuBsp48+JyPelXAq8ds6K5Du9gd', }.each_pair do |keytype, keydata| it "should be able to parse a #{keytype} key entry" do comment = 'sample_key' record = @provider_class.parse_line("#{keytype} #{keydata} #{comment}") record.should_not be_nil record[:name].should == comment record[:key].should == keydata record[:type].should == keytype end end + describe "prefetch_hook" do + let(:path) { '/path/to/keyfile' } + let(:input) do + { :type => 'rsa', + :key => 'KEYDATA', + :name => '', + :record_type => :parsed, + :target => path, + } + end + it "adds an indexed name to unnamed resources" do + @provider_class.prefetch_hook([input])[0][:name].should =~ /^#{path}:unnamed-\d+/ + end + end + end describe provider_class, :unless => Puppet.features.microsoft_windows? do before :each do @resource = Puppet::Type.type(:ssh_authorized_key).new(:name => "foo", :user => "random_bob") @provider = provider_class.new(@resource) provider_class.stubs(:filetype).returns(Puppet::Util::FileType::FileTypeRam) Puppet::Util::SUIDManager.stubs(:asuser).yields provider_class.initvars end describe "when flushing" do before :each do # Stub file and directory operations Dir.stubs(:mkdir) File.stubs(:chmod) File.stubs(:chown) end describe "and both a user and a target have been specified" do before :each do Puppet::Util.stubs(:uid).with("random_bob").returns 12345 @resource[:user] = "random_bob" target = "/tmp/.ssh_dir/place_to_put_authorized_keys" @resource[:target] = target end it "should create the directory" do Puppet::FileSystem.stubs(:exist?).with("/tmp/.ssh_dir").returns false Dir.expects(:mkdir).with("/tmp/.ssh_dir", 0700) @provider.flush end it "should absolutely not chown the directory to the user" do uid = Puppet::Util.uid("random_bob") File.expects(:chown).never @provider.flush end it "should absolutely not chown the key file to the user" do uid = Puppet::Util.uid("random_bob") File.expects(:chown).never @provider.flush end it "should chmod the key file to 0600" do File.expects(:chmod).with(0600, "/tmp/.ssh_dir/place_to_put_authorized_keys") @provider.flush end end describe "and a user has been specified with no target" do before :each do @resource[:user] = "nobody" # # I'd like to use random_bob here and something like # # File.stubs(:expand_path).with("~random_bob/.ssh").returns "/users/r/random_bob/.ssh" # # but mocha objects strenuously to stubbing File.expand_path # so I'm left with using nobody. @dir = File.expand_path("~nobody/.ssh") end it "should create the directory if it doesn't exist" do Puppet::FileSystem.stubs(:exist?).with(@dir).returns false Dir.expects(:mkdir).with(@dir,0700) @provider.flush end it "should not create or chown the directory if it already exist" do Puppet::FileSystem.stubs(:exist?).with(@dir).returns false Dir.expects(:mkdir).never @provider.flush end it "should absolutely not chown the directory to the user if it creates it" do Puppet::FileSystem.stubs(:exist?).with(@dir).returns false Dir.stubs(:mkdir).with(@dir,0700) uid = Puppet::Util.uid("nobody") File.expects(:chown).never @provider.flush end it "should not create or chown the directory if it already exist" do Puppet::FileSystem.stubs(:exist?).with(@dir).returns false Dir.expects(:mkdir).never File.expects(:chown).never @provider.flush end it "should absolutely not chown the key file to the user" do uid = Puppet::Util.uid("nobody") File.expects(:chown).never @provider.flush end it "should chmod the key file to 0600" do File.expects(:chmod).with(0600, File.expand_path("~nobody/.ssh/authorized_keys")) @provider.flush end end describe "and a target has been specified with no user" do it "should raise an error" do @resource = Puppet::Type.type(:ssh_authorized_key).new(:name => "foo", :target => "/tmp/.ssh_dir/place_to_put_authorized_keys") @provider = provider_class.new(@resource) proc { @provider.flush }.should raise_error end end describe "and an invalid user has been specified with no target" do it "should catch an exception and raise a Puppet error" do @resource[:user] = "thisusershouldnotexist" lambda { @provider.flush }.should raise_error(Puppet::Error) end end end end diff --git a/spec/unit/type/user_spec.rb b/spec/unit/type/user_spec.rb index f5a351752..974054309 100755 --- a/spec/unit/type/user_spec.rb +++ b/spec/unit/type/user_spec.rb @@ -1,514 +1,519 @@ #! /usr/bin/env ruby # encoding: UTF-8 require 'spec_helper' describe Puppet::Type.type(:user) do before :each do @provider_class = described_class.provide(:simple) do has_features :manages_expiry, :manages_password_age, :manages_passwords, :manages_solaris_rbac, :manages_shell mk_resource_methods def create; end def delete; end def exists?; get(:ensure) != :absent; end def flush; end def self.instances; []; end end described_class.stubs(:defaultprovider).returns @provider_class end it "should be able to create an instance" do described_class.new(:name => "foo").should_not be_nil end it "should have an allows_duplicates feature" do described_class.provider_feature(:allows_duplicates).should_not be_nil end it "should have a manages_homedir feature" do described_class.provider_feature(:manages_homedir).should_not be_nil end it "should have a manages_passwords feature" do described_class.provider_feature(:manages_passwords).should_not be_nil end it "should have a manages_solaris_rbac feature" do described_class.provider_feature(:manages_solaris_rbac).should_not be_nil end it "should have a manages_expiry feature" do described_class.provider_feature(:manages_expiry).should_not be_nil end it "should have a manages_password_age feature" do described_class.provider_feature(:manages_password_age).should_not be_nil end it "should have a system_users feature" do described_class.provider_feature(:system_users).should_not be_nil end it "should have a manages_shell feature" do described_class.provider_feature(:manages_shell).should_not be_nil end describe :managehome do let (:provider) { @provider_class.new(:name => 'foo', :ensure => :absent) } let (:instance) { described_class.new(:name => 'foo', :provider => provider) } it "defaults to false" do instance[:managehome].should be_false end it "can be set to false" do instance[:managehome] = 'false' end it "cannot be set to true for a provider that does not manage homedirs" do provider.class.stubs(:manages_homedir?).returns false expect { instance[:managehome] = 'yes' }.to raise_error(Puppet::Error, /can not manage home directories/) end it "can be set to true for a provider that does manage homedirs" do provider.class.stubs(:manages_homedir?).returns true instance[:managehome] = 'yes' end end describe "instances" do it "should delegate existence questions to its provider" do @provider = @provider_class.new(:name => 'foo', :ensure => :absent) instance = described_class.new(:name => "foo", :provider => @provider) instance.exists?.should == false @provider.set(:ensure => :present) instance.exists?.should == true end end properties = [:ensure, :uid, :gid, :home, :comment, :shell, :password, :password_min_age, :password_max_age, :groups, :roles, :auths, :profiles, :project, :keys, :expiry] properties.each do |property| it "should have a #{property} property" do described_class.attrclass(property).ancestors.should be_include(Puppet::Property) end it "should have documentation for its #{property} property" do described_class.attrclass(property).doc.should be_instance_of(String) end end list_properties = [:groups, :roles, :auths] list_properties.each do |property| it "should have a list '#{property}'" do described_class.attrclass(property).ancestors.should be_include(Puppet::Property::List) end end it "should have an ordered list 'profiles'" do described_class.attrclass(:profiles).ancestors.should be_include(Puppet::Property::OrderedList) end it "should have key values 'keys'" do described_class.attrclass(:keys).ancestors.should be_include(Puppet::Property::KeyValue) end describe "when retrieving all current values" do before do @provider = @provider_class.new(:name => 'foo', :ensure => :present, :uid => 15, :gid => 15) @user = described_class.new(:name => "foo", :uid => 10, :provider => @provider) end it "should return a hash containing values for all set properties" do @user[:gid] = 10 values = @user.retrieve [@user.property(:uid), @user.property(:gid)].each { |property| values.should be_include(property) } end it "should set all values to :absent if the user is absent" do @user.property(:ensure).expects(:retrieve).returns :absent @user.property(:uid).expects(:retrieve).never @user.retrieve[@user.property(:uid)].should == :absent end it "should include the result of retrieving each property's current value if the user is present" do @user.retrieve[@user.property(:uid)].should == 15 end end describe "when managing the ensure property" do it "should support a :present value" do expect { described_class.new(:name => 'foo', :ensure => :present) }.to_not raise_error end it "should support an :absent value" do expect { described_class.new(:name => 'foo', :ensure => :absent) }.to_not raise_error end it "should call :create on the provider when asked to sync to the :present state" do @provider = @provider_class.new(:name => 'foo', :ensure => :absent) @provider.expects(:create) described_class.new(:name => 'foo', :ensure => :present, :provider => @provider).parameter(:ensure).sync end it "should call :delete on the provider when asked to sync to the :absent state" do @provider = @provider_class.new(:name => 'foo', :ensure => :present) @provider.expects(:delete) described_class.new(:name => 'foo', :ensure => :absent, :provider => @provider).parameter(:ensure).sync end describe "and determining the current state" do it "should return :present when the provider indicates the user exists" do @provider = @provider_class.new(:name => 'foo', :ensure => :present) described_class.new(:name => 'foo', :ensure => :absent, :provider => @provider).parameter(:ensure).retrieve.should == :present end it "should return :absent when the provider indicates the user does not exist" do @provider = @provider_class.new(:name => 'foo', :ensure => :absent) described_class.new(:name => 'foo', :ensure => :present, :provider => @provider).parameter(:ensure).retrieve.should == :absent end end end describe "when managing the uid property" do it "should convert number-looking strings into actual numbers" do described_class.new(:name => 'foo', :uid => '50')[:uid].should == 50 end it "should support UIDs as numbers" do described_class.new(:name => 'foo', :uid => 50)[:uid].should == 50 end it "should support :absent as a value" do described_class.new(:name => 'foo', :uid => :absent)[:uid].should == :absent end end describe "when managing the gid" do it "should support :absent as a value" do described_class.new(:name => 'foo', :gid => :absent)[:gid].should == :absent end it "should convert number-looking strings into actual numbers" do described_class.new(:name => 'foo', :gid => '50')[:gid].should == 50 end it "should support GIDs specified as integers" do described_class.new(:name => 'foo', :gid => 50)[:gid].should == 50 end it "should support groups specified by name" do described_class.new(:name => 'foo', :gid => 'foo')[:gid].should == 'foo' end describe "when testing whether in sync" do it "should return true if no 'should' values are set" do # this is currently not the case because gid has no default value, so we would never even # call insync? on that property if param = described_class.new(:name => 'foo').parameter(:gid) param.must be_safe_insync(500) end end it "should return true if any of the specified groups are equal to the current integer" do Puppet::Util.expects(:gid).with("foo").returns 300 Puppet::Util.expects(:gid).with("bar").returns 500 described_class.new(:name => 'baz', :gid => [ 'foo', 'bar' ]).parameter(:gid).must be_safe_insync(500) end it "should return false if none of the specified groups are equal to the current integer" do Puppet::Util.expects(:gid).with("foo").returns 300 Puppet::Util.expects(:gid).with("bar").returns 500 described_class.new(:name => 'baz', :gid => [ 'foo', 'bar' ]).parameter(:gid).must_not be_safe_insync(700) end end describe "when syncing" do it "should use the first found, specified group as the desired value and send it to the provider" do Puppet::Util.expects(:gid).with("foo").returns nil Puppet::Util.expects(:gid).with("bar").returns 500 @provider = @provider_class.new(:name => 'foo') resource = described_class.new(:name => 'foo', :provider => @provider, :gid => [ 'foo', 'bar' ]) @provider.expects(:gid=).with 500 resource.parameter(:gid).sync end end end describe "when managing groups" do it "should support a singe group" do expect { described_class.new(:name => 'foo', :groups => 'bar') }.to_not raise_error end it "should support multiple groups as an array" do expect { described_class.new(:name => 'foo', :groups => [ 'bar' ]) }.to_not raise_error expect { described_class.new(:name => 'foo', :groups => [ 'bar', 'baz' ]) }.to_not raise_error end it "should not support a comma separated list" do expect { described_class.new(:name => 'foo', :groups => 'bar,baz') }.to raise_error(Puppet::Error, /Group names must be provided as an array/) end it "should not support an empty string" do expect { described_class.new(:name => 'foo', :groups => '') }.to raise_error(Puppet::Error, /Group names must not be empty/) end describe "when testing is in sync" do before :each do # the useradd provider uses a single string to represent groups and so does Puppet::Property::List when converting to should values @provider = @provider_class.new(:name => 'foo', :groups => 'a,b,e,f') end it "should not care about order" do @property = described_class.new(:name => 'foo', :groups => [ 'a', 'c', 'b' ]).property(:groups) @property.must be_safe_insync([ 'a', 'b', 'c' ]) @property.must be_safe_insync([ 'a', 'c', 'b' ]) @property.must be_safe_insync([ 'b', 'a', 'c' ]) @property.must be_safe_insync([ 'b', 'c', 'a' ]) @property.must be_safe_insync([ 'c', 'a', 'b' ]) @property.must be_safe_insync([ 'c', 'b', 'a' ]) end it "should merge current value and desired value if membership minimal" do @instance = described_class.new(:name => 'foo', :groups => [ 'a', 'c', 'b' ], :provider => @provider) @instance[:membership] = :minimum @instance[:groups].should == 'a,b,c,e,f' end it "should not treat a subset of groups insync if membership inclusive" do @instance = described_class.new(:name => 'foo', :groups => [ 'a', 'c', 'b' ], :provider => @provider) @instance[:membership] = :inclusive @instance[:groups].should == 'a,b,c' end end end describe "when managing expiry" do it "should fail if given an invalid date" do expect { described_class.new(:name => 'foo', :expiry => "200-20-20") }.to raise_error(Puppet::Error, /Expiry dates must be YYYY-MM-DD/) end end describe "when managing minimum password age" do it "should accept a negative minimum age" do expect { described_class.new(:name => 'foo', :password_min_age => '-1') }.to_not raise_error end it "should fail with an empty minimum age" do expect { described_class.new(:name => 'foo', :password_min_age => '') }.to raise_error(Puppet::Error, /minimum age must be provided as a number/) end end describe "when managing maximum password age" do it "should accept a negative maximum age" do expect { described_class.new(:name => 'foo', :password_max_age => '-1') }.to_not raise_error end it "should fail with an empty maximum age" do expect { described_class.new(:name => 'foo', :password_max_age => '') }.to raise_error(Puppet::Error, /maximum age must be provided as a number/) end end describe "when managing passwords" do before do @password = described_class.new(:name => 'foo', :password => 'mypass').parameter(:password) end it "should not include the password in the change log when adding the password" do @password.change_to_s(:absent, "mypass").should_not be_include("mypass") end it "should not include the password in the change log when changing the password" do @password.change_to_s("other", "mypass").should_not be_include("mypass") end it "should redact the password when displaying the old value" do @password.is_to_s("currentpassword").should =~ /^\[old password hash redacted\]$/ end it "should redact the password when displaying the new value" do @password.should_to_s("newpassword").should =~ /^\[new password hash redacted\]$/ end it "should fail if a ':' is included in the password" do expect { described_class.new(:name => 'foo', :password => "some:thing") }.to raise_error(Puppet::Error, /Passwords cannot include ':'/) end it "should allow the value to be set to :absent" do expect { described_class.new(:name => 'foo', :password => :absent) }.to_not raise_error end end describe "when managing comment on Ruby 1.9", :if => String.method_defined?(:encode) do it "should force value encoding to ASCII-8BIT" do value = 'abcd™' value.encoding.should == Encoding::UTF_8 user = described_class.new(:name => 'foo', :comment => value) user[:comment].encoding.should == Encoding::ASCII_8BIT user[:comment].should == value.force_encoding(Encoding::ASCII_8BIT) end end describe "when manages_solaris_rbac is enabled" do it "should support a :role value for ensure" do expect { described_class.new(:name => 'foo', :ensure => :role) }.to_not raise_error end end describe "when user has roles" do it "should autorequire roles" do testuser = described_class.new(:name => "testuser", :roles => ['testrole'] ) testrole = described_class.new(:name => "testrole") config = Puppet::Resource::Catalog.new :testing do |conf| [testuser, testrole].each { |resource| conf.add_resource resource } end Puppet::Type::User::ProviderDirectoryservice.stubs(:get_macosx_version_major).returns "10.5" rel = testuser.autorequire[0] rel.source.ref.should == testrole.ref rel.target.ref.should == testuser.ref end end describe "when setting shell" do before :each do @shell_provider_class = described_class.provide(:shell_manager) do has_features :manages_shell mk_resource_methods def create; check_valid_shell;end def shell=(value); check_valid_shell; end def delete; end def exists?; get(:ensure) != :absent; end def flush; end def self.instances; []; end def check_valid_shell; end end described_class.stubs(:defaultprovider).returns @shell_provider_class end it "should call :check_valid_shell on the provider when changing shell value" do @provider = @shell_provider_class.new(:name => 'foo', :shell => '/bin/bash', :ensure => :present) @provider.expects(:check_valid_shell) resource = described_class.new(:name => 'foo', :shell => '/bin/zsh', :provider => @provider) Puppet::Util::Storage.stubs(:load) Puppet::Util::Storage.stubs(:store) catalog = Puppet::Resource::Catalog.new catalog.add_resource resource catalog.apply end it "should call :check_valid_shell on the provider when changing ensure from present to absent" do @provider = @shell_provider_class.new(:name => 'foo', :shell => '/bin/bash', :ensure => :absent) @provider.expects(:check_valid_shell) resource = described_class.new(:name => 'foo', :shell => '/bin/zsh', :provider => @provider) Puppet::Util::Storage.stubs(:load) Puppet::Util::Storage.stubs(:store) catalog = Puppet::Resource::Catalog.new catalog.add_resource resource catalog.apply end end describe "when purging ssh keys" do it "should not accept a keyfile with a relative path" do expect { described_class.new(:name => "a", :purge_ssh_keys => "keys") }.to raise_error(Puppet::Error, /Paths to keyfiles must be absolute, not keys/) end context "with a home directory specified" do it "should accept true" do described_class.new(:name => "a", :home => "/tmp", :purge_ssh_keys => true) end it "should accept the ~ wildcard" do described_class.new(:name => "a", :home => "/tmp", :purge_ssh_keys => "~/keys") end it "should accept the %h wildcard" do described_class.new(:name => "a", :home => "/tmp", :purge_ssh_keys => "%h/keys") end it "raises when given a relative path" do expect { described_class.new(:name => "a", :home => "/tmp", :purge_ssh_keys => "keys") }.to raise_error(Puppet::Error, /Paths to keyfiles must be absolute/) end end context "with no home directory specified" do it "should not accept true" do expect { described_class.new(:name => "a", :purge_ssh_keys => true) }.to raise_error(Puppet::Error, /purge_ssh_keys can only be true for users with a defined home directory/) end it "should not accept the ~ wildcard" do expect { described_class.new(:name => "a", :purge_ssh_keys => "~/keys") }.to raise_error(Puppet::Error, /meta character ~ or %h only allowed for users with a defined home directory/) end it "should not accept the %h wildcard" do expect { described_class.new(:name => "a", :purge_ssh_keys => "%h/keys") }.to raise_error(Puppet::Error, /meta character ~ or %h only allowed for users with a defined home directory/) end end context "with a valid parameter" do let(:paths) do [ "/dev/null", "/tmp/keyfile" ].map { |path| File.expand_path(path) } end subject do res = described_class.new(:name => "test", :purge_ssh_keys => paths) res.catalog = Puppet::Resource::Catalog.new res end it "should not just return from generate" do subject.expects :find_unmanaged_keys subject.generate end it "should check each keyfile for readability" do paths.each do |path| File.expects(:readable?).with(path) end subject.generate end end describe "generated keys" do subject do res = described_class.new(:name => "test_user_name", :purge_ssh_keys => purge_param) res.catalog = Puppet::Resource::Catalog.new res end context "when purging is disabled" do let(:purge_param) { false } its(:generate) { should be_empty } end context "when purging is enabled" do let(:purge_param) { my_fixture('authorized_keys') } let(:resources) { subject.generate } it "should contain a resource for each key" do names = resources.collect { |res| res.name } names.should include("key1 name") names.should include("keyname2") end it "should not include keys in comment lines" do names = resources.collect { |res| res.name } names.should_not include("keyname3") end + it "should generate names for unnamed keys" do + names = resources.collect { |res| res.name } + fixture_path = File.join(my_fixture_dir, 'authorized_keys') + names.should include("#{fixture_path}:unnamed-1") + end it "should each have a value for the user property" do resources.map { |res| res[:user] }.reject { |user_name| user_name == "test_user_name" }.should be_empty end end end end end