require "sfn" require "graph" module Sfn class Command # Graph command class Graph < Command autoload :Provider, "sfn/command/graph/provider" include Sfn::CommandModule::Base include Sfn::CommandModule::Template include Sfn::CommandModule::Stack # Valid graph styles GRAPH_STYLES = [ "creation", "dependency", ] # Generate graph def execute! config[:print_only] = true validate_graph_style! file = load_template_file provider = Bogo::Utility.camel(file.provider).to_sym if Provider.constants.include?(provider) graph_const = Provider.const_get(provider) ui.debug "Loading provider graph implementation - #{graph_const}" extend graph_const @outputs = Smash.new ui.info "Template resource graph generation - Style: #{ui.color(config[:graph_style], :bold)}" if config[:file] ui.puts " -> path: #{config[:file]}" end template_dump = file.compile.sparkle_dump!.to_smash run_action "Pre-processing template for graphing" do output_discovery(template_dump, @outputs, nil, nil) ui.debug "Output remapping results from pre-processing:" @outputs.each_pair do |o_key, o_value| ui.debug "#{o_key} -> #{o_value}" end nil end graph = nil run_action "Generating resource graph" do graph = generate_graph(template_dump) nil end run_action "Writing graph result" do FileUtils.mkdir_p(File.dirname(config[:output_file])) if config[:output_type] == "dot" File.open("#{config[:output_file]}.dot", "w") do |o_file| o_file.puts graph.to_s end else graph.save config[:output_file], config[:output_type] end nil end else valid_providers = Provider.constants.sort.map { |provider| Bogo::Utility.snake(provider) }.join("`, `") ui.error "Graphing for provider `#{file.provider}` not currently supported." ui.error "Currently supported providers: `#{valid_providers}`." end end def generate_graph(template, args = {}) graph = ::Graph.new @root_graph = graph unless @root_graph graph.graph_attribs << ::Graph::Attribute.new("overlap = false") graph.graph_attribs << ::Graph::Attribute.new("splines = true") graph.graph_attribs << ::Graph::Attribute.new("pack = true") graph.graph_attribs << ::Graph::Attribute.new('start = "random"') if args[:name] graph.name = "cluster_#{args[:name]}" labelnode_key = "cluster_#{args[:name]}" graph.plaintext << graph.node(labelnode_key) graph.node(labelnode_key).label args[:name] else graph.name = "root" end edge_detection(template, graph, args[:name].to_s.sub("cluster_", ""), args.fetch(:resource_names, [])) graph end def colorize(string) hash = string.chars.inject(0) do |memo, chr| if memo + chr.ord > 127 (memo - chr.ord).abs else memo + chr.ord end end color = "#" 3.times do |i| color << (255 ^ hash).to_s(16) new_val = hash + (hash * (1 / (i + 1.to_f))).to_i if hash * (i + 1) < 127 hash = new_val else hash = hash / (i + 1) end end color end def validate_graph_style! if config[:luckymike] ui.warn "Detected luckymike power override. Forcing `dependency` style!" config[:graph_style] = "dependency" end config[:graph_style] = config[:graph_style].to_s unless GRAPH_STYLES.include?(config[:graph_style]) raise ArgumentError.new "Invalid graph style provided `#{config[:graph_style]}`. Valid: `#{GRAPH_STYLES.join("`, `")}`" end end end end end