lono 5.2.8 → 5.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|