diff --git a/lib/puppet-languageserver-sidecar/facter_helper.rb b/lib/puppet-languageserver-sidecar/facter_helper.rb new file mode 100644 index 00000000..815620af --- /dev/null +++ b/lib/puppet-languageserver-sidecar/facter_helper.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module PuppetLanguageServerSidecar + module FacterHelper + def self.current_environment + begin + env = Puppet.lookup(:environments).get!(Puppet.settings[:environment]) + return env unless env.nil? + rescue Puppet::Environments::EnvironmentNotFound + PuppetLanguageServerSidecar.log_message(:warning, "[FacterHelper::current_environment] Unable to load environment #{Puppet.settings[:environment]}") + rescue StandardError => e + PuppetLanguageServerSidecar.log_message(:warning, "[FacterHelper::current_environment] Error loading environment #{Puppet.settings[:environment]}: #{e}") + end + Puppet.lookup(:current_environment) + end + + def self.retrieve_facts(_cache, _options = {}) + require 'puppet/indirector/facts/facter' + + PuppetLanguageServerSidecar.log_message(:debug, '[FacterHelper::retrieve_facts] Starting') + facts = PuppetLanguageServer::Sidecar::Protocol::Facts.new + begin + req = Puppet::Indirector::Request.new(:facts, :find, 'language_server', nil, environment: current_environment) + result = Puppet::Node::Facts::Facter.new.find(req) + facts.from_h!(result.values) + rescue StandardError => e + PuppetLanguageServerSidecar.log_message(:error, "[FacterHelper::_load_facts] Error loading facts #{e.message} #{e.backtrace}") + rescue LoadError => e + PuppetLanguageServerSidecar.log_message(:error, "[FacterHelper::_load_facts] Error loading facts (LoadError) #{e.message} #{e.backtrace}") + end + + PuppetLanguageServerSidecar.log_message(:debug, "[FacterHelper::retrieve_facts] Finished loading #{facts.keys.count} facts") + facts + end + end +end diff --git a/lib/puppet-languageserver-sidecar/puppet_modulepath_monkey_patches.rb b/lib/puppet-languageserver-sidecar/puppet_modulepath_monkey_patches.rb index b51d9c34..e1ffced7 100644 --- a/lib/puppet-languageserver-sidecar/puppet_modulepath_monkey_patches.rb +++ b/lib/puppet-languageserver-sidecar/puppet_modulepath_monkey_patches.rb @@ -122,3 +122,21 @@ def modulepath result end end + +# Inject the workspace into the facter search paths +require 'puppet/indirector/facts/facter' +class Puppet::Node::Facts::Facter # rubocop:disable Style/ClassAndModuleChildren + class << self + alias_method :original_setup_search_paths, :setup_search_paths + def setup_search_paths(request) + result = original_setup_search_paths(request) + return result unless PuppetLanguageServerSidecar::Workspace.has_module_metadata? + + additional_dirs = %w[lib plugins].map { |path| File.join(PuppetLanguageServerSidecar::Workspace.root_path, path, 'facter') } + .select { |path| FileTest.directory?(path) } + + return result if additional_dirs.empty? + Facter.search(*additional_dirs) + end + end +end diff --git a/lib/puppet-languageserver/sidecar_protocol.rb b/lib/puppet-languageserver/sidecar_protocol.rb index fd6267b8..286b58b1 100644 --- a/lib/puppet-languageserver/sidecar_protocol.rb +++ b/lib/puppet-languageserver/sidecar_protocol.rb @@ -520,6 +520,27 @@ def list_for_object_class(klass) raise "Unknown object class #{klass.name}" end end + + class Facts < Hash + include Base + + def from_h!(value) + value.keys.each { |key| self[key] = value[key] } + self + end + + def to_json(*options) + ::JSON.generate(to_h, options) + end + + def from_json!(json_string) + obj = ::JSON.parse(json_string) + obj.each do |key, value| + self[key] = value + end + self + end + end end end end diff --git a/lib/puppet_languageserver_sidecar.rb b/lib/puppet_languageserver_sidecar.rb index d1e05d21..bda1b5d5 100644 --- a/lib/puppet_languageserver_sidecar.rb +++ b/lib/puppet_languageserver_sidecar.rb @@ -80,6 +80,7 @@ def self.require_gems(options) puppet_parser_helper sidecar_protocol_extensions workspace + facter_helper ] # Load files based on feature flags @@ -117,6 +118,7 @@ def self.require_gems(options) workspace_datatypes workspace_functions workspace_types + facts ].freeze class CommandLineParser @@ -389,6 +391,14 @@ def self.execute(options) PuppetLanguageServerSidecar::PuppetHelper.retrieve_types(null_cache) end + when 'facts' + # Can't cache for facts + cache = PuppetLanguageServerSidecar::Cache::Null.new + # Inject the workspace etc. if present + injected = inject_workspace_as_module + inject_workspace_as_environment unless injected + PuppetLanguageServerSidecar::FacterHelper.retrieve_facts(cache) + else log_message(:error, "Unknown action #{options[:action]}. Expected one of #{ACTION_LIST}") end diff --git a/spec/languageserver-sidecar/fixtures/real_agent/cache/facts.d/.gitkeep b/spec/languageserver-sidecar/fixtures/real_agent/cache/facts.d/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/spec/languageserver-sidecar/fixtures/real_agent/cache/lib/facter/.gitkeep b/spec/languageserver-sidecar/fixtures/real_agent/cache/lib/facter/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/spec/languageserver-sidecar/fixtures/real_agent/cache/lib/facter/fixture_agent_custom_fact.rb b/spec/languageserver-sidecar/fixtures/real_agent/cache/lib/facter/fixture_agent_custom_fact.rb new file mode 100644 index 00000000..39fbfa1f --- /dev/null +++ b/spec/languageserver-sidecar/fixtures/real_agent/cache/lib/facter/fixture_agent_custom_fact.rb @@ -0,0 +1,5 @@ +Facter.add('fixture_agent_custom_fact') do + setcode do + 'fixture_agent_custom_fact_value' + end +end diff --git a/spec/languageserver-sidecar/fixtures/valid_environment_workspace/site/private/facts.d/fixture_environment_external_fact.yaml b/spec/languageserver-sidecar/fixtures/valid_environment_workspace/site/private/facts.d/fixture_environment_external_fact.yaml new file mode 100644 index 00000000..9fcdc2f8 --- /dev/null +++ b/spec/languageserver-sidecar/fixtures/valid_environment_workspace/site/private/facts.d/fixture_environment_external_fact.yaml @@ -0,0 +1,2 @@ +--- +fixture_environment_external_fact: "fixture_environment_external_fact_value" diff --git a/spec/languageserver-sidecar/fixtures/valid_environment_workspace/site/private/lib/facter/fixture_environment_custom_fact.rb b/spec/languageserver-sidecar/fixtures/valid_environment_workspace/site/private/lib/facter/fixture_environment_custom_fact.rb new file mode 100644 index 00000000..53aedf48 --- /dev/null +++ b/spec/languageserver-sidecar/fixtures/valid_environment_workspace/site/private/lib/facter/fixture_environment_custom_fact.rb @@ -0,0 +1,5 @@ +Facter.add('fixture_environment_custom_fact') do + setcode do + 'fixture_environment_custom_fact_value' + end +end diff --git a/spec/languageserver-sidecar/fixtures/valid_module_workspace/facts.d/fixture_module_external_fact.yaml b/spec/languageserver-sidecar/fixtures/valid_module_workspace/facts.d/fixture_module_external_fact.yaml new file mode 100644 index 00000000..7802161e --- /dev/null +++ b/spec/languageserver-sidecar/fixtures/valid_module_workspace/facts.d/fixture_module_external_fact.yaml @@ -0,0 +1,2 @@ +--- +fixture_module_external_fact: "fixture_module_external_fact_value" diff --git a/spec/languageserver-sidecar/fixtures/valid_module_workspace/lib/facter/fixture_module_custom_fact.rb b/spec/languageserver-sidecar/fixtures/valid_module_workspace/lib/facter/fixture_module_custom_fact.rb new file mode 100644 index 00000000..eb912f98 --- /dev/null +++ b/spec/languageserver-sidecar/fixtures/valid_module_workspace/lib/facter/fixture_module_custom_fact.rb @@ -0,0 +1,5 @@ +Facter.add('fixture_module_custom_fact') do + setcode do + 'fixture_module_custom_fact_value' + end +end diff --git a/spec/languageserver-sidecar/integration/puppet-languageserver-sidecar/facter_helper_spec.rb b/spec/languageserver-sidecar/integration/puppet-languageserver-sidecar/facter_helper_spec.rb new file mode 100644 index 00000000..06c67fae --- /dev/null +++ b/spec/languageserver-sidecar/integration/puppet-languageserver-sidecar/facter_helper_spec.rb @@ -0,0 +1,90 @@ +require 'spec_helper' +require 'open3' + +describe 'PuppetLanguageServerSidecar::FacterHelper' do + let(:subject) { PuppetLanguageServerSidecar::FacterHelper } + + def run_sidecar(cmd_options) + # Use a new array so we don't affect the original cmd_options) + cmd = cmd_options.dup + + # Append the puppet test-fixtures + cmd << '--puppet-settings' + cmd << "--vardir,#{File.join($fixtures_dir, 'real_agent', 'cache')},--confdir,#{File.join($fixtures_dir, 'real_agent', 'confdir')}" + + cmd.unshift('puppet-languageserver-sidecar') + cmd.unshift('ruby') + stdout, _stderr, status = Open3.capture3(*cmd) + + raise "Expected exit code of 0, but got #{status.exitstatus} #{_stderr}" unless status.exitstatus.zero? + return stdout.bytes.pack('U*') + end + + let(:default_fact_names) { ['hostname', 'fixture_agent_custom_fact'] } + let(:module_fact_names) { ['fixture_module_custom_fact', 'fixture_module_external_fact'] } + let(:environment_fact_names) { ['fixture_environment_custom_fact', 'fixture_environment_external_fact'] } + + describe 'when running facts action' do + let (:cmd_options) { ['--action', 'facts'] } + + it 'should return a deserializable facts object with all default facts' do + result = run_sidecar(cmd_options) + deserial = PuppetLanguageServer::Sidecar::Protocol::Facts.new + expect { deserial.from_json!(result) }.to_not raise_error + + default_fact_names.each do |name| + expect(deserial).to include(name) + end + + module_fact_names.each do |name| + expect(deserial).not_to include(name) + end + end + end + + context 'given a workspace containing a module' do + # Test fixtures used is fixtures/valid_module_workspace + let(:workspace) { File.join($fixtures_dir, 'valid_module_workspace') } + + describe 'when running facts action' do + let (:cmd_options) { ['--action', 'facts', '--local-workspace', workspace] } + + it 'should return a deserializable facts object with default facts and workspace facts' do + result = run_sidecar(cmd_options) + deserial = PuppetLanguageServer::Sidecar::Protocol::Facts.new + expect { deserial.from_json!(result) }.to_not raise_error + + default_fact_names.each do |name| + expect(deserial).to include(name) + end + + module_fact_names.each do |name| + expect(deserial).to include(name) + end + end + end + end + + context 'given a workspace containing an environment.conf' do + # Test fixtures used is fixtures/valid_environment_workspace + let(:workspace) { File.join($fixtures_dir, 'valid_environment_workspace') } + + describe 'when running facts action' do + let (:cmd_options) { ['--action', 'facts', '--local-workspace', workspace] } + + it 'should return a deserializable facts object with default facts and workspace facts' do + result = run_sidecar(cmd_options) + deserial = PuppetLanguageServer::Sidecar::Protocol::Facts.new + expect { deserial.from_json!(result) }.to_not raise_error + + default_fact_names.each do |name| + expect(deserial).to include(name) + end + + environment_fact_names.each do |name| + expect(deserial).to include(name) + end + end + end + end +end diff --git a/spec/languageserver/unit/puppet-languageserver/sidecar_protocol_spec.rb b/spec/languageserver/unit/puppet-languageserver/sidecar_protocol_spec.rb index e7ea1ff1..a62388a8 100644 --- a/spec/languageserver/unit/puppet-languageserver/sidecar_protocol_spec.rb +++ b/spec/languageserver/unit/puppet-languageserver/sidecar_protocol_spec.rb @@ -98,7 +98,7 @@ deserial = subject_klass.new.from_json!(serial) subject.keys.each do |key| - expect(deserial[key]).to eq(deserial[key]) + expect(deserial[key]).to eq(subject[key]) end end end @@ -131,6 +131,30 @@ end end + describe 'Facts' do + let(:subject_klass) { PuppetLanguageServer::Sidecar::Protocol::Facts } + let(:subject) { + value = subject_klass.new + value['val1_' + rand(1000).to_s] = rand(1000).to_s + value['val2_' + rand(1000).to_s] = rand(1000).to_s + value['val3_' + rand(1000).to_s] = rand(1000).to_s + value + } + + it_should_behave_like 'a base Sidecar Protocol object' + + describe '#from_json!' do + it "should deserialize a serialized value" do + serial = subject.to_json + deserial = subject_klass.new.from_json!(serial) + + subject.keys.each do |key| + expect(deserial[key]).to eq(subject[key]) + end + end + end + end + describe 'NodeGraph' do let(:subject_klass) { PuppetLanguageServer::Sidecar::Protocol::NodeGraph } let(:subject) {