From 82f768b02b0f279b6a8a61c3d75104d04301ef93 Mon Sep 17 00:00:00 2001 From: Glenn Sarti Date: Sun, 12 Jan 2020 21:33:42 +0800 Subject: [PATCH] (GH-213) Gather facts using the Sidecar Previously the facts where evaluated only within the Editor Service process however this meant the workspace couldn't be injected. This commit adds a new action to the Sidecar process to evaluate the facts. The Sidecar has the required monkey patches to inject the workspace into the fact search directories. This commit also adds tests and fixtures to ensure that custom and external facts can be resolved. --- .../facter_helper.rb | 36 ++++++++ .../puppet_modulepath_monkey_patches.rb | 18 ++++ lib/puppet-languageserver/sidecar_protocol.rb | 21 +++++ lib/puppet_languageserver_sidecar.rb | 10 +++ .../real_agent/cache/facts.d/.gitkeep | 0 .../real_agent/cache/lib/facter/.gitkeep | 0 .../lib/facter/fixture_agent_custom_fact.rb | 5 ++ .../fixture_environment_external_fact.yaml | 2 + .../facter/fixture_environment_custom_fact.rb | 5 ++ .../facts.d/fixture_module_external_fact.yaml | 2 + .../lib/facter/fixture_module_custom_fact.rb | 5 ++ .../facter_helper_spec.rb | 90 +++++++++++++++++++ .../sidecar_protocol_spec.rb | 26 +++++- 13 files changed, 219 insertions(+), 1 deletion(-) create mode 100644 lib/puppet-languageserver-sidecar/facter_helper.rb delete mode 100644 spec/languageserver-sidecar/fixtures/real_agent/cache/facts.d/.gitkeep delete mode 100644 spec/languageserver-sidecar/fixtures/real_agent/cache/lib/facter/.gitkeep create mode 100644 spec/languageserver-sidecar/fixtures/real_agent/cache/lib/facter/fixture_agent_custom_fact.rb create mode 100644 spec/languageserver-sidecar/fixtures/valid_environment_workspace/site/private/facts.d/fixture_environment_external_fact.yaml create mode 100644 spec/languageserver-sidecar/fixtures/valid_environment_workspace/site/private/lib/facter/fixture_environment_custom_fact.rb create mode 100644 spec/languageserver-sidecar/fixtures/valid_module_workspace/facts.d/fixture_module_external_fact.yaml create mode 100644 spec/languageserver-sidecar/fixtures/valid_module_workspace/lib/facter/fixture_module_custom_fact.rb create mode 100644 spec/languageserver-sidecar/integration/puppet-languageserver-sidecar/facter_helper_spec.rb 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) {