Skip to content

Commit

Permalink
(GH-40) Add Node Graph generation to Sidecar capability
Browse files Browse the repository at this point in the history
This commit adds the ability for the sidecar to generate a node graph for a
manifest file on disk. The path the manifest is passed via the source Action
Parameter.  This commit also adds the required sidecar protocol additions and
tests for the behaviour.
  • Loading branch information
glennsarti committed Oct 3, 2018
1 parent ba53039 commit adc75fa
Show file tree
Hide file tree
Showing 8 changed files with 255 additions and 0 deletions.
17 changes: 17 additions & 0 deletions lib/puppet-languageserver-sidecar.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
cache/null
cache/filesystem
puppet_helper
puppet_parser_helper
puppet_monkey_patches
sidecar_protocol_extensions
workspace
Expand All @@ -46,6 +47,7 @@ def self.version
default_classes
default_functions
default_types
node_graph
resource_list
workspace_classes
workspace_functions
Expand Down Expand Up @@ -183,6 +185,21 @@ def self.execute(options)
cache = options[:disable_cache] ? PuppetLanguageServerSidecar::Cache::Null.new : PuppetLanguageServerSidecar::Cache::FileSystem.new
PuppetLanguageServerSidecar::PuppetHelper.retrieve_types(cache)

when 'node_graph'
inject_workspace_as_module
result = PuppetLanguageServerSidecar::Protocol::NodeGraph.new
if options[:action_parameters]['source'].nil?
log_message(:error, 'Missing source action parameter')
return result.set_error('Missing source action parameter')
end
begin
manifest = File.open(options[:action_parameters]['source'], 'r:UTF-8') { |f| f.read }
PuppetLanguageServerSidecar::PuppetParserHelper.compile_node_graph(manifest)
rescue StandardError => ex
log_message(:error, "Unable to compile the manifest. #{ex}")
result.set_error("Unable to compile the manifest. #{ex}")
end

when 'resource_list'
inject_workspace_as_module
typename = options[:action_parameters]['typename']
Expand Down
71 changes: 71 additions & 0 deletions lib/puppet-languageserver-sidecar/puppet_parser_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
module PuppetLanguageServerSidecar
module PuppetParserHelper
def self.compile_node_graph(content)
result = PuppetLanguageServerSidecar::Protocol::NodeGraph.new

begin
# The fontsize is inserted in the puppet code. Need to remove it so the client can render appropriately. Need to
# set it to blank. The graph label is set to editorservices so that we can do text replacement client side to inject the
# appropriate styling.
options = {
'fontsize' => '""',
'name' => 'editorservices'
}
node_graph = compile_to_pretty_relationship_graph(content)
if node_graph.vertices.count.zero?
result.set_error('There were no resources created in the node graph. Is there an include statement missing?')
else
result.dot_content = node_graph.to_dot(options)
end
rescue StandardError => exception
result.set_error("Error while parsing the file. #{exception}")
end

result
end

# Reference - https://github.com/puppetlabs/puppet/blob/master/spec/lib/puppet_spec/compiler.rb
def self.compile_to_catalog(string, node = Puppet::Node.new('test'))
Puppet[:code] = string
# see lib/puppet/indirector/catalog/compiler.rb#filter
Puppet::Parser::Compiler.compile(node).filter(&:virtual?)
end

def self.compile_to_ral(manifest, node = Puppet::Node.new('test'))
# Add the node facts if they don't already exist
node.merge(Facter.to_hash) if node.facts.nil?

catalog = compile_to_catalog(manifest, node)
ral = catalog.to_ral
ral.finalize
ral
end

def self.compile_to_relationship_graph(manifest, prioritizer = Puppet::Graph::SequentialPrioritizer.new)
ral = compile_to_ral(manifest)
graph = Puppet::Graph::RelationshipGraph.new(prioritizer)
graph.populate_from(ral)
graph
end

def self.compile_to_pretty_relationship_graph(manifest, prioritizer = Puppet::Graph::SequentialPrioritizer.new)
graph = compile_to_relationship_graph(manifest, prioritizer)

# Remove vertexes which just clutter the graph

# Remove all of the Puppet::Type::Whit nodes. This is an internal only class
list = graph.vertices.select { |node| node.is_a?(Puppet::Type::Whit) }
list.each { |node| graph.remove_vertex!(node) }

# Remove all of the Puppet::Type::Schedule nodes
list = graph.vertices.select { |node| node.is_a?(Puppet::Type::Schedule) }
list.each { |node| graph.remove_vertex!(node) }

# Remove all of the Puppet::Type::Filebucket nodes
list = graph.vertices.select { |node| node.is_a?(Puppet::Type::Filebucket) }
list.each { |node| graph.remove_vertex!(node) }

graph
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

module PuppetLanguageServerSidecar
module Protocol
class NodeGraph < PuppetLanguageServer::Sidecar::Protocol::NodeGraph
def set_error(message) # rubocop:disable Naming/AccessorMethodName
self.error_content = message
self.dot_content = ''
self
end
end

class PuppetClass < PuppetLanguageServer::Sidecar::Protocol::PuppetClass
def self.from_puppet(name, item)
obj = PuppetLanguageServer::Sidecar::Protocol::PuppetClass.new
Expand Down
21 changes: 21 additions & 0 deletions lib/puppet-languageserver/sidecar_protocol.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,27 @@ def child_type
end
end

class NodeGraph
include Base

attr_accessor :dot_content
attr_accessor :error_content

def to_json(*options)
{
'dot_content' => dot_content,
'error_content' => error_content
}.to_json(options)
end

def from_json!(json_string)
obj = JSON.parse(json_string)
self.dot_content = obj['dot_content']
self.error_content = obj['error_content']
self
end
end

class PuppetClass < BasePuppetObject
# TODO: Doc, parameters?
end
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require 'spec_helper'
require 'open3'
require 'tempfile'

def run_sidecar(cmd_options)
cmd_options << '--no-cache'
Expand All @@ -20,6 +21,19 @@ def child_with_key(array, key)
return idx.nil? ? nil : array[idx]
end

def with_temporary_file(content)
tempfile = Tempfile.new("langserver-sidecar")
tempfile.open

tempfile.write(content)

tempfile.close

yield tempfile.path
ensure
tempfile.delete if tempfile
end

RSpec::Matchers.define :contain_child_with_key do |key|
match do |actual|
!(actual.index { |item| item.key == key }).nil?
Expand Down Expand Up @@ -92,6 +106,26 @@ def child_with_key(array, key)
# Test fixtures used is fixtures/valid_module_workspace
let(:workspace) { File.join($fixtures_dir, 'valid_module_workspace') }

describe 'when running node_graph action' do
let (:cmd_options) { ['--action', 'node_graph', '--local-workspace',workspace] }

it 'should return a deserializable node graph' do
# The fixture type is only present in the local workspace
with_temporary_file("fixture { 'test':\n}") do |filepath|
action_params = PuppetLanguageServer::Sidecar::Protocol::ActionParams.new()
action_params['source'] = filepath

result = run_sidecar(cmd_options.concat(['--action-parameters', action_params.to_json]))

deserial = PuppetLanguageServer::Sidecar::Protocol::NodeGraph.new()
expect { deserial.from_json!(result) }.to_not raise_error

expect(deserial.dot_content).to match(/Fixture\[test\]/)
expect(deserial.error_content.to_s).to eq('')
end
end
end

describe 'when running workspace_classes action' do
let (:cmd_options) { ['--action', 'workspace_classes', '--local-workspace',workspace] }

Expand Down Expand Up @@ -154,6 +188,25 @@ def child_with_key(array, key)
end
end

describe 'when running node_graph action' do
let (:cmd_options) { ['--action', 'node_graph'] }

it 'should return a deserializable node graph' do
with_temporary_file("user { 'test':\nensure => present\n}") do |filepath|
action_params = PuppetLanguageServer::Sidecar::Protocol::ActionParams.new()
action_params['source'] = filepath

result = run_sidecar(cmd_options.concat(['--action-parameters', action_params.to_json]))

deserial = PuppetLanguageServer::Sidecar::Protocol::NodeGraph.new()
expect { deserial.from_json!(result) }.to_not raise_error

expect(deserial.dot_content).to_not eq('')
expect(deserial.error_content.to_s).to eq('')
end
end
end

describe 'when running resource_list action' do
let (:cmd_options) { ['--action', 'resource_list'] }

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
require 'spec_helper'

describe 'PuppetLanguageServerSidecar::PuppetParserHelper' do
let (:subject) { PuppetLanguageServerSidecar::PuppetParserHelper }

describe '#compile_node_graph' do
context 'a valid manifest' do
let(:manifest) { "user { 'test':\nensure => present\n}\n "}

it 'should compile succesfully' do
result = subject.compile_node_graph(manifest)
expect(result).to_not be_nil
# Make sure it's a DOT graph file
expect(result.dot_content).to match(/digraph/)
# Make sure the resource is there
expect(result.dot_content).to match(/User\[test\]/)
# Make sure the fontsize is set to empty
expect(result.dot_content).to match(/fontsize = \"\"/)
# Make sure the label is editorservices
expect(result.dot_content).to match(/label = \"editorservices\"/)
# Expect no errors
expect(result.error_content.to_s).to eq('')
end
end

context 'a valid manifest with no resources' do
let(:manifest) { "" }

it 'should compile with an error' do
result = subject.compile_node_graph(manifest)
expect(result).to_not be_nil
expect(result.dot_content).to eq("")
expect(result.error_content).to match(/no resources created in the node graph/)
end
end

context 'an invalid manifest' do
let(:manifest) { "I am an invalid manifest" }

it 'should compile with an error' do
result = subject.compile_node_graph(manifest)
expect(result).to_not be_nil
expect(result.dot_content).to eq("")
expect(result.error_content).to match(/Error while parsing the file./)
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@
end
end

describe 'NodeGraph' do
let(:subject_klass) { PuppetLanguageServerSidecar::Protocol::NodeGraph }
let(:subject) { subject_klass.new }

it "instance should respond to set_error" do
expect(subject).to respond_to(:set_error)
result = subject.set_error('test_error')
expect(result.dot_content).to eq('')
expect(result.error_content).to eq('test_error')
end
end

describe 'PuppetClass' do
let(:subject_klass) { PuppetLanguageServerSidecar::Protocol::PuppetClass }
let(:subject) { subject_klass.new }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
end

basepuppetobject_properties = [:key, :calling_source, :source, :line, :char, :length]
nodegraph_properties = [:dot_content, :error_content]
puppetclass_properties = []
puppetfunction_properties = [:doc, :arity, :type]
puppettype_properties = [:doc, :attributes]
Expand Down Expand Up @@ -109,6 +110,30 @@
end
end

describe 'NodeGraph' do
let(:subject_klass) { PuppetLanguageServer::Sidecar::Protocol::NodeGraph }
let(:subject) {
value = subject_klass.new
value.dot_content = 'dot_content_' + rand(1000).to_s
value.error_content = 'error_content_' + rand(1000).to_s
value
}

it_should_behave_like 'a base Sidecar Protocol object'

describe '#from_json!' do
nodegraph_properties.each do |testcase|
it "should deserialize a serialized #{testcase} value" do
#require 'pry'; binding.pry
serial = subject.to_json
deserial = subject_klass.new.from_json!(serial)

expect(deserial.send(testcase)).to eq(subject.send(testcase))
end
end
end
end

describe 'PuppetClass' do
let(:subject_klass) { PuppetLanguageServer::Sidecar::Protocol::PuppetClass }
let(:subject) {
Expand Down

0 comments on commit adc75fa

Please sign in to comment.