lono 5.2.8 → 5.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +2 -0
- data/lib/lono/cfn.rb +9 -13
- data/lib/lono/cfn/preview.rb +2 -149
- data/lib/lono/cfn/preview/changeset.rb +154 -0
- data/lib/lono/cfn/{diff.rb → preview/codediff.rb} +6 -14
- data/lib/lono/cfn/preview/diff_viewer.rb +19 -0
- data/lib/lono/cfn/preview/param.rb +73 -0
- data/lib/lono/cfn/update.rb +15 -8
- data/lib/lono/inspector.rb +1 -1
- data/lib/lono/inspector/base.rb +30 -36
- data/lib/lono/inspector/graph.rb +100 -99
- data/lib/lono/inspector/summary.rb +48 -59
- data/lib/lono/output_template.rb +35 -0
- data/lib/lono/param/generator.rb +38 -9
- data/lib/lono/seed/base.rb +5 -3
- data/lib/lono/template/context.rb +2 -0
- data/lib/lono/template/context/helpers.rb +14 -0
- data/lib/lono/template/context/ssm_fetcher.rb +23 -0
- data/lib/lono/template/dsl/builder/parameter.rb +4 -4
- data/lib/lono/template/dsl/builder/resource.rb +10 -5
- data/lib/lono/template/dsl/builder/resource/property_mover.rb +19 -0
- data/lib/lono/version.rb +1 -1
- data/lono.gemspec +1 -0
- metadata +24 -4
- data/lib/lono/help/cfn/diff.md +0 -24
@@ -0,0 +1,73 @@
|
|
1
|
+
module Lono::Cfn::Preview
|
2
|
+
class Param < Lono::Cfn::Base
|
3
|
+
delegate :required_parameters, :optional_parameters, :parameters, :data,
|
4
|
+
to: :output_template
|
5
|
+
|
6
|
+
include DiffViewer
|
7
|
+
include Lono::AwsServices
|
8
|
+
|
9
|
+
def run
|
10
|
+
return unless stack_exists?(@stack_name)
|
11
|
+
|
12
|
+
puts "Parameter Diff Preview:".color(:green)
|
13
|
+
if @options[:noop]
|
14
|
+
puts "NOOP CloudFormation parameters preview for #{@stack_name} update"
|
15
|
+
return
|
16
|
+
end
|
17
|
+
|
18
|
+
params = generate_all
|
19
|
+
write_to_tmp(new_path, params)
|
20
|
+
write_to_tmp(existing_path, existing_parameters)
|
21
|
+
|
22
|
+
show_diff(existing_path, new_path)
|
23
|
+
end
|
24
|
+
|
25
|
+
def existing_parameters
|
26
|
+
resp = cfn.describe_stacks(stack_name: @stack_name)
|
27
|
+
stack = resp.stacks.first
|
28
|
+
parameters = stack.parameters
|
29
|
+
|
30
|
+
# Remove optional parameters if they match already. Produces better diff.
|
31
|
+
optional = optional_parameters.map do |logical_id, attributes|
|
32
|
+
{
|
33
|
+
"ParameterKey" => logical_id,
|
34
|
+
"ParameterValue" => attributes["Default"],
|
35
|
+
}
|
36
|
+
end
|
37
|
+
converted = convert_to_cfn_format(parameters)
|
38
|
+
converted - optional
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
def output_template
|
43
|
+
Lono::OutputTemplate.new(@blueprint, @template)
|
44
|
+
end
|
45
|
+
memoize :output_template
|
46
|
+
|
47
|
+
def write_to_tmp(path, list)
|
48
|
+
converted = convert_to_cfn_format(list)
|
49
|
+
text = JSON.pretty_generate(converted)
|
50
|
+
FileUtils.mkdir_p(File.dirname(path))
|
51
|
+
IO.write(path, text)
|
52
|
+
end
|
53
|
+
|
54
|
+
def convert_to_cfn_format(list)
|
55
|
+
camelized = list.map(&:to_h).map do |h|
|
56
|
+
h.transform_keys {|k| k.to_s.camelize}
|
57
|
+
end
|
58
|
+
camelized.sort_by { |h| h["ParameterKey"] }
|
59
|
+
end
|
60
|
+
|
61
|
+
def existing_path
|
62
|
+
"#{tmp_base}/existing.json"
|
63
|
+
end
|
64
|
+
|
65
|
+
def new_path
|
66
|
+
"#{tmp_base}/new.json"
|
67
|
+
end
|
68
|
+
|
69
|
+
def tmp_base
|
70
|
+
"/tmp/lono/params-preview"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
data/lib/lono/cfn/update.rb
CHANGED
@@ -27,16 +27,17 @@ class Lono::Cfn
|
|
27
27
|
|
28
28
|
options = @options.merge(mute_params: true, mute_using: true, keep: true)
|
29
29
|
# create new copy of preview when update_stack is called because of IAM retry logic
|
30
|
-
|
30
|
+
changeset_preview = Lono::Cfn::Preview::Changeset.new(@stack_name, options)
|
31
31
|
|
32
32
|
error = nil
|
33
|
-
|
34
|
-
|
33
|
+
param_preview.run if @options[:param_preview]
|
34
|
+
codediff_preview.run if @options[:codediff_preview]
|
35
|
+
changeset_preview.run if @options[:changeset_preview]
|
35
36
|
are_you_sure?(@stack_name, :update)
|
36
37
|
|
37
38
|
if @options[:change_set] # defaults to this
|
38
|
-
message << " via change set: #{
|
39
|
-
|
39
|
+
message << " via change set: #{changeset_preview.change_set_name}"
|
40
|
+
changeset_preview.execute_change_set
|
40
41
|
else
|
41
42
|
standard_update(params)
|
42
43
|
end
|
@@ -57,12 +58,18 @@ class Lono::Cfn
|
|
57
58
|
cfn.update_stack(params)
|
58
59
|
rescue Aws::CloudFormation::Errors::ValidationError => e
|
59
60
|
puts "ERROR: #{e.message}".red
|
60
|
-
|
61
|
+
false
|
61
62
|
end
|
62
63
|
end
|
63
64
|
|
64
|
-
def
|
65
|
-
|
65
|
+
def codediff_preview
|
66
|
+
Lono::Cfn::Preview::Codediff.new(@stack_name, @options.merge(mute_params: true, mute_using: true))
|
66
67
|
end
|
68
|
+
memoize :codediff_preview
|
69
|
+
|
70
|
+
def param_preview
|
71
|
+
Lono::Cfn::Preview::Param.new(@stack_name, @options)
|
72
|
+
end
|
73
|
+
memoize :param_preview
|
67
74
|
end
|
68
75
|
end
|
data/lib/lono/inspector.rb
CHANGED
data/lib/lono/inspector/base.rb
CHANGED
@@ -1,48 +1,42 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
module Lono::Inspector
|
2
|
+
class Base
|
3
|
+
delegate :required_parameters, :optional_parameters, :parameters, :data,
|
4
|
+
to: :output_template
|
3
5
|
|
4
|
-
|
5
|
-
|
6
|
-
end
|
6
|
+
extend Memoist
|
7
|
+
include Lono::Blueprint::Root
|
7
8
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
@blueprint_name = blueprint
|
9
|
+
def initialize(blueprint, template, options)
|
10
|
+
@blueprint, @template, @options = blueprint, template, options
|
11
|
+
end
|
12
12
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
13
|
+
def run
|
14
|
+
blueprints = Lono::Blueprint::Find.one_or_all(@blueprint)
|
15
|
+
blueprints.each do |blueprint|
|
16
|
+
@blueprint = blueprint # intentional overwrite
|
17
|
+
generate_templates
|
18
|
+
set_blueprint_root(blueprint)
|
19
|
+
templates = @template_name ? [@template_name] : all_templates
|
20
|
+
templates.each do |template_name|
|
21
|
+
perform(template_name)
|
22
|
+
end
|
18
23
|
end
|
19
24
|
end
|
20
|
-
end
|
21
|
-
|
22
|
-
def generate_templates
|
23
|
-
Lono::Template::Generator.new(@blueprint_name, @options.clone.merge(quiet: false)).run
|
24
|
-
end
|
25
25
|
|
26
|
-
|
27
|
-
|
28
|
-
Dir.glob("#{templates_path}/**").map do |path|
|
29
|
-
path.sub("#{templates_path}/", '').sub('.yml','') # template_name
|
26
|
+
def generate_templates
|
27
|
+
Lono::Template::Generator.new(@blueprint, @options.clone.merge(quiet: false)).run
|
30
28
|
end
|
31
|
-
end
|
32
29
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
30
|
+
def all_templates
|
31
|
+
templates_path = "#{Lono.config.output_path}/#{@blueprint}/templates"
|
32
|
+
Dir.glob("#{templates_path}/**").map do |path|
|
33
|
+
path.sub("#{templates_path}/", '').sub('.yml','') # template_name
|
34
|
+
end
|
35
|
+
end
|
38
36
|
|
39
|
-
|
40
|
-
|
41
|
-
def check_template_exists(template_path)
|
42
|
-
unless File.exist?(template_path)
|
43
|
-
puts "The template #{template_path} does not exist. Are you sure you use the right template name? The template name does not require the extension.".color(:red)
|
44
|
-
exit 1
|
37
|
+
def output_template
|
38
|
+
Lono::OutputTemplate.new(@blueprint, @template)
|
45
39
|
end
|
40
|
+
memoize :output_template
|
46
41
|
end
|
47
|
-
|
48
42
|
end
|
data/lib/lono/inspector/graph.rb
CHANGED
@@ -1,124 +1,125 @@
|
|
1
1
|
require "yaml"
|
2
2
|
require "graph"
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
def perform(template_name)
|
11
|
-
# little dirty but @template_name is used in data method
|
12
|
-
# so we dont have to pass it to the data method
|
13
|
-
@template_name = template_name
|
14
|
-
|
15
|
-
puts "Generating dependencies tree for template #{@template_name}..."
|
16
|
-
return if @options[:noop]
|
17
|
-
|
18
|
-
# First loop through top level nodes and build set depends_on property
|
19
|
-
node_list = [] # top level node list
|
20
|
-
resources = data["Resources"]
|
21
|
-
resources.each do |logical_id, resource|
|
22
|
-
node = Node.new(logical_id)
|
23
|
-
node.depends_on = normalize_depends_on(resource)
|
24
|
-
node.resource_type = normalize_resource_type(resource)
|
25
|
-
node_list << node
|
4
|
+
module Lono::Inspector
|
5
|
+
class Graph < Base
|
6
|
+
def initialize(blueprint, template, options)
|
7
|
+
super
|
8
|
+
@nodes = [] # lookup map
|
26
9
|
end
|
27
10
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
11
|
+
def perform(template)
|
12
|
+
# little dirty but @template is used in data method so we dont have to pass it to the data method
|
13
|
+
@template = template
|
14
|
+
|
15
|
+
puts "Generating dependencies tree for template #{@template}..."
|
16
|
+
return if @options[:noop]
|
17
|
+
|
18
|
+
# First loop through top level nodes and build set depends_on property
|
19
|
+
node_list = [] # top level node list
|
20
|
+
resources = data["Resources"]
|
21
|
+
resources.each do |logical_id, resource|
|
22
|
+
node = Node.new(logical_id)
|
23
|
+
node.depends_on = normalize_depends_on(resource)
|
24
|
+
node.resource_type = normalize_resource_type(resource)
|
25
|
+
node_list << node
|
26
|
+
end
|
34
27
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
node_list.each
|
39
|
-
|
40
|
-
|
41
|
-
puts "CloudFormation Dependencies graph generated."
|
42
|
-
end
|
43
|
-
end
|
28
|
+
# Now that we have loop through all the top level resources once
|
29
|
+
# we can use the depends_on attribute on each node and set the
|
30
|
+
# children property since the identity nodes are in memory.
|
31
|
+
node_list.each do |node|
|
32
|
+
node.children = normalize_children(node_list, node)
|
33
|
+
end
|
44
34
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
35
|
+
# At this point we have a tree of nodes.
|
36
|
+
if @options[:display] == "text"
|
37
|
+
puts "CloudFormation Dependencies:"
|
38
|
+
node_list.each { |node| print_tree(node) }
|
39
|
+
else
|
40
|
+
print_graph(node_list)
|
41
|
+
puts "CloudFormation Dependencies graph generated."
|
42
|
+
end
|
43
|
+
end
|
50
44
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
45
|
+
# normalized DependOn attribute to an Array of Strings
|
46
|
+
def normalize_depends_on(resource)
|
47
|
+
dependencies = resource["DependOn"] || []
|
48
|
+
[dependencies].flatten
|
49
|
+
end
|
55
50
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
def normalize_children(node_list, node)
|
60
|
-
kids = []
|
61
|
-
node.depends_on.each do |dependent_logical_id|
|
62
|
-
node = node_list.find { |n| n.name == dependent_logical_id }
|
63
|
-
kids << node
|
51
|
+
def normalize_resource_type(resource)
|
52
|
+
type = resource["Type"]
|
53
|
+
type.sub("AWS::", "") # strip out AWS to make less verbose
|
64
54
|
end
|
65
|
-
kids
|
66
|
-
end
|
67
55
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
56
|
+
# It is possible with bad CloudFormation templates that the dependency is not
|
57
|
+
# resolved, but we wont deal with that. Users can validate their CloudFormation
|
58
|
+
# template before using this tool.
|
59
|
+
def normalize_children(node_list, node)
|
60
|
+
kids = []
|
61
|
+
node.depends_on.each do |dependent_logical_id|
|
62
|
+
node = node_list.find { |n| n.name == dependent_logical_id }
|
63
|
+
kids << node
|
64
|
+
end
|
65
|
+
kids
|
73
66
|
end
|
74
|
-
end
|
75
67
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
68
|
+
def print_tree(node, depth=0)
|
69
|
+
spacing = " " * depth
|
70
|
+
puts "#{spacing}#{node.name}"
|
71
|
+
node.children.each do |node|
|
72
|
+
print_tree(node, depth+1)
|
73
|
+
end
|
74
|
+
end
|
81
75
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
76
|
+
def print_graph(node_list)
|
77
|
+
check_graphviz_installed
|
78
|
+
digraph do
|
79
|
+
# graph_attribs << 'size="6,6"'
|
80
|
+
node_attribs << lightblue << filled
|
81
|
+
|
82
|
+
node_list.each do |n|
|
83
|
+
node(n.graph_name)
|
84
|
+
n.children.each do |child|
|
85
|
+
edge n.graph_name, child.graph_name
|
86
|
+
end
|
86
87
|
end
|
87
|
-
end
|
88
88
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
89
|
+
random = (0...8).map { (65 + rand(26)).chr }.join
|
90
|
+
path = "/tmp/cloudformation-depends-on-#{random}"
|
91
|
+
save path, "png"
|
92
|
+
# Check if open command exists and use it to open the image.
|
93
|
+
system "open #{path}.png" if system("type open > /dev/null")
|
94
|
+
end
|
94
95
|
end
|
95
|
-
end
|
96
96
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
97
|
+
# Check if Graphiz is installed and prints a user friendly message if it is not installed.
|
98
|
+
# Provide instructions if on macosx.
|
99
|
+
def check_graphviz_installed
|
100
|
+
installed = system("type dot > /dev/null") # dot is a command that is part of the graphviz package
|
101
|
+
unless installed
|
102
|
+
puts "It appears that the Graphviz is not installed. Please install it to generate the graph."
|
103
|
+
if RUBY_PLATFORM =~ /darwin/
|
104
|
+
puts "You can install Graphviz with homebrew:"
|
105
|
+
puts " brew install brew install graphviz"
|
106
|
+
end
|
107
|
+
exit 1
|
106
108
|
end
|
107
|
-
exit 1
|
108
109
|
end
|
109
|
-
end
|
110
110
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
111
|
+
class Node
|
112
|
+
attr_accessor :name, :resource_type, :children, :depends_on
|
113
|
+
def initialize(name)
|
114
|
+
@name = name
|
115
|
+
@children = []
|
116
|
+
@depends_on = []
|
117
|
+
end
|
118
118
|
|
119
|
-
|
120
|
-
|
121
|
-
|
119
|
+
def graph_name
|
120
|
+
type = "(#{resource_type})" if resource_type
|
121
|
+
[name, type].compact.join("\n")
|
122
|
+
end
|
122
123
|
end
|
123
124
|
end
|
124
125
|
end
|
@@ -1,76 +1,65 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
1
|
+
module Lono::Inspector
|
2
|
+
class Summary < Base
|
3
|
+
def perform(template)
|
4
|
+
# little dirty but @template is used in data method so we dont have to pass it to the data method
|
5
|
+
@template = template
|
6
6
|
|
7
|
-
|
8
|
-
|
7
|
+
puts "=> CloudFormation Template Summary for template #{@template.color(:sienna)}:"
|
8
|
+
return if @options[:noop]
|
9
9
|
|
10
|
-
|
10
|
+
print_parameters_summary
|
11
11
|
|
12
|
-
|
13
|
-
|
14
|
-
|
12
|
+
puts "Resources:"
|
13
|
+
print_resource_types
|
14
|
+
end
|
15
15
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
16
|
+
def print_parameters_summary
|
17
|
+
if parameters.empty?
|
18
|
+
puts "There are no parameters in this template."
|
19
|
+
else
|
20
|
+
print_parameters("Required Parameters", required_parameters)
|
21
|
+
print_parameters("Optional Parameters", optional_parameters)
|
22
|
+
end
|
22
23
|
end
|
23
|
-
end
|
24
24
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
25
|
+
def print_parameters(label, parameters)
|
26
|
+
puts "#{label}:"
|
27
|
+
if parameters.empty?
|
28
|
+
puts " There are no #{label.downcase} parameters"
|
29
|
+
else
|
30
|
+
parameters.each do |logical_id, p|
|
31
|
+
output = " #{logical_id} (#{p["Type"]})"
|
32
|
+
if p["Default"]
|
33
|
+
output << " Default: #{p["Default"]}"
|
34
|
+
end
|
35
|
+
puts output
|
34
36
|
end
|
35
|
-
puts output
|
36
37
|
end
|
37
38
|
end
|
38
|
-
end
|
39
|
-
|
40
|
-
def required_parameters
|
41
|
-
parameters.reject { |logical_id, p| p["Default"] }
|
42
|
-
end
|
43
|
-
|
44
|
-
def optional_parameters
|
45
|
-
parameters.select { |logical_id, p| p["Default"] }
|
46
|
-
end
|
47
|
-
|
48
|
-
def parameters
|
49
|
-
data["Parameters"] || []
|
50
|
-
end
|
51
39
|
|
52
|
-
|
53
|
-
|
54
|
-
|
40
|
+
def resource_types
|
41
|
+
resources = data["Resources"]
|
42
|
+
return unless resources
|
55
43
|
|
56
|
-
|
57
|
-
|
58
|
-
|
44
|
+
types = Hash.new(0)
|
45
|
+
resources.each do |logical_id, resource|
|
46
|
+
types[resource["Type"]] += 1
|
47
|
+
end
|
48
|
+
types
|
59
49
|
end
|
60
|
-
types
|
61
|
-
end
|
62
50
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
51
|
+
def print_resource_types
|
52
|
+
unless resource_types
|
53
|
+
puts "No resources found."
|
54
|
+
return
|
55
|
+
end
|
68
56
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
57
|
+
types = resource_types.sort_by {|r| r[1] * -1} # Hash -> 2D Array
|
58
|
+
types.each do |a|
|
59
|
+
type, count = a
|
60
|
+
printf "%3s %s\n", count, type
|
61
|
+
end
|
62
|
+
printf "%3s %s\n", resource_types.size, "Total"
|
73
63
|
end
|
74
|
-
printf "%3s %s\n", resource_types.size, "Total"
|
75
64
|
end
|
76
65
|
end
|