mkstack 1.0.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: aa22c1f1c57234678eefa866bc219909dc00334d91d6d4daf70be9762233d51c
4
+ data.tar.gz: c5a1f8095d6b34d467fafd443fbc9605b206a42ef70e65f47ec9290ed8783ca3
5
+ SHA512:
6
+ metadata.gz: b0a0588f47fbc38f7efddb0175afbfacb7d40604a41094a9bc705a26fc6edc154c00bc9f2146e1a19d3326d1d7bfdbb2a6f2de9918a5f8a67ca11e06e202d286
7
+ data.tar.gz: b23350d9abe01bb969d9dbf617d7025f5572481e526b37a7632a6ec8fb46c7a64aebb92266166105bfd55ffd73f5187b642620d04bdf4ede746ceeb99bf696ee
Binary file
Binary file
@@ -0,0 +1,106 @@
1
+ #!/usr/local/bin/ruby
2
+
3
+ require "logger"
4
+ require "optparse"
5
+ require "mkstack"
6
+
7
+
8
+ ##################################################
9
+ # Main
10
+
11
+ # Define logger
12
+ $logger = Logger.new(STDERR)
13
+ $logger.level = Logger::WARN
14
+ $logger.formatter = proc { |s, d, p, m|
15
+ "[%s] %5s [%5s] - %s\n" % [ d.strftime("%Y-%m-%d %H:%M:%S"), s, caller(4, 1).first.split(":")[1], m ]
16
+ }
17
+
18
+
19
+ # Default options
20
+ options = {}
21
+ options[:erb] = true
22
+ options[:format] = "json"
23
+
24
+
25
+ # Command line options
26
+ opts = OptionParser.new do |p|
27
+ desc_padding = " " * (p.summary_indent.length + p.summary_width)
28
+
29
+ p.banner = "Usage: #{p.program_name} [ options ] file1 [ file2... ]"
30
+
31
+ # Help
32
+ p.on("-h", "--help", "Display this message") { puts opts ; exit }
33
+ p.separator(" ")
34
+
35
+ # Verbosity
36
+ p.on("-d", "--debug", "Show debug messages") { $logger.level = Logger::DEBUG }
37
+ p.on("-v", "--verbose", "Be verbose") { $logger.level = Logger::INFO }
38
+ p.on("-q", "--quiet", "Only show errors") { $logger.level = Logger::ERROR }
39
+ p.on("-s", "--silient", "Don't show any log messages") { $logger.level = Logger::FATAL }
40
+ p.separator(" ")
41
+
42
+ # Output
43
+ p.on("-o", "--output=FILE", "Print final template to FILE") { |x| options[:save] = x }
44
+ p.on("%s Use '-' for stdout" % [ desc_padding ])
45
+ p.on("-f", "--format=FORMAT", [ "json", "yaml" ], "Print as FORMAT") { |x| options[:format] = x }
46
+ p.on("%s Supported formats: json (default), yaml" % [ desc_padding ])
47
+ p.separator(" ")
48
+
49
+ # Operations
50
+ p.on("--erb", "--[no-]erb", "Perform ERB processing (default is true)") { |x| options[:erb] = x }
51
+ p.on("--validate", "Call ValidateTemplate after merging") { options[:validate] = true }
52
+ end
53
+
54
+ files = opts.parse!.uniq
55
+ opts.parse "--help" if files.count == 0
56
+
57
+ template = MkStack::Template.new(options[:format])
58
+
59
+
60
+ # Merge files
61
+ files.each do |file|
62
+ begin
63
+ Dir.chdir(File.dirname(file)) { template.merge(File.basename(file), options[:erb]) }
64
+ rescue KeyError => e
65
+ $logger.warn { "#{file}: already parsed" }
66
+ rescue Exception => e
67
+ $logger.error { e.message }
68
+ end
69
+ end
70
+
71
+
72
+ # Check limits
73
+ $logger.debug { "Checking limits" }
74
+
75
+ template.sections.each do |name, section|
76
+ $logger.warn { "#{name} limit exceeded: (#{section.length} > #{section.limit})" } if section.exceeds_limit?
77
+ end
78
+
79
+ $logger.warn { "At least one Resources member must be defined" } if template["Resources"].length == 0
80
+ $logger.warn { "Template too large (#{template.length} > #{template.limit})" } if template.exceeds_limit?
81
+
82
+
83
+ # Validate template
84
+ if options[:validate] then
85
+ begin
86
+ $logger.info { "Validating #{template.format} template" }
87
+
88
+ $logger.debug { template.validate }
89
+ rescue Exception => e
90
+ $logger.error { e.message }
91
+ end
92
+ end
93
+
94
+
95
+ # Save template
96
+ if options[:save] then
97
+ begin
98
+ $stdout = File.new(options[:save], File::CREAT | File::WRONLY) unless options[:save] == "-"
99
+
100
+ $logger.info { "Saving #{template.format} to #{$stdout.path}" } unless $stdout == STDOUT
101
+
102
+ puts template.pp
103
+ rescue Exception => e
104
+ $logger.error { e.message }
105
+ end
106
+ end
@@ -0,0 +1,81 @@
1
+ require_relative "mkstack/template"
2
+
3
+ =begin rdoc
4
+
5
+ Merge multiple CloudFormation template files into a single template.
6
+ Each file may be in either JSON or YAML format.
7
+
8
+ Get started with <i>template = MkStack::Template.new</i>
9
+
10
+ == ERB
11
+
12
+ By default all files are run through an ERB (Embedded RuBy) processor.
13
+
14
+ <% desc = "awesome" %>
15
+
16
+ AWSTemplateFormatVersion: "2010-09-09"
17
+ Description: My <%= desc %> CloudFormation template
18
+
19
+ It is safe to leave this enabled. If a file doesn't have any ERB tags
20
+ it is passed through untouched.
21
+
22
+ == Include
23
+
24
+ MkStack searches each file for a section named <b>Include</b>, which should
25
+ be a list of filenames. These function the same as adding the listed
26
+ files on the command line.
27
+
28
+ === JSON
29
+
30
+ "Include" : [
31
+ "foo.yaml",
32
+ "bar.json"
33
+ ]
34
+
35
+ === YAML
36
+
37
+ Include:
38
+ - foo.yaml
39
+ - bar.json
40
+
41
+ == ERB and Include working together
42
+
43
+ MkStack uses a single <i>binding</i> for all files. This allows ERB
44
+ tags defined in one file to be referenced in subsequent files.
45
+
46
+ === foo.yaml
47
+
48
+ Include:
49
+ - bar.json
50
+
51
+ <% tags_json = %q{
52
+ "Tags": [
53
+ { "Key" : "application", "Value" : "mkstack" }
54
+ ]
55
+ }
56
+ %>
57
+
58
+ === bar.json
59
+
60
+ {
61
+ "Resources" : {
62
+ "sg": {
63
+ "Type" : "AWS::EC2::SecurityGroup",
64
+ "Properties" : {
65
+ "GroupDescription" : { "Fn::Sub" : "Security Group for ${application}" }
66
+ <%= tags_json %>
67
+ }
68
+ }
69
+ }
70
+ }
71
+
72
+ Note that foo.yaml is processed <i>before</i> bar.json.
73
+
74
+ == See Also
75
+
76
+ MkStack::Template
77
+ MkStack::Section
78
+ =end
79
+
80
+ module MkStack
81
+ end
@@ -0,0 +1,32 @@
1
+ module MkStack
2
+ ##################################################
3
+ # A CloudFormation template section
4
+ class Section
5
+ attr_reader :name, :limit
6
+ attr_accessor :contents
7
+
8
+ # * name: The section's name (Resources, Outputs, etc.)
9
+ # * type: The section's type (Hash or String)
10
+ # * limit: The AWS limit for this section, if any
11
+ def initialize(name, type, limit = nil)
12
+ @name = name
13
+ @limit = limit
14
+
15
+ @contents = type.new
16
+ end
17
+
18
+ # Merge or override a section snippet
19
+ def merge(contents)
20
+ raise TypeError.new("#{contents.class} != #{@contents.class}") if contents.class != @contents.class
21
+
22
+ return @contents.merge!(contents) if @contents.respond_to?(:merge!)
23
+ @contents = contents
24
+ end
25
+
26
+ # Return the length of the section's contents
27
+ def length; @contents.length; end
28
+
29
+ # Check if the section exceeds the AWS limit
30
+ def exceeds_limit?; @limit && length > @limit; end
31
+ end
32
+ end
@@ -0,0 +1,178 @@
1
+ require_relative "section"
2
+
3
+ require "erb"
4
+ require "json"
5
+ require "yaml"
6
+
7
+ module MkStack
8
+ ##################################################
9
+ # A CloudFormation template
10
+ class Template
11
+ attr_reader :sections, :limit, :format
12
+
13
+ def initialize(format = "json")
14
+ @format = format
15
+
16
+ @sections = {
17
+ "AWSTemplateFormatVersion" => Section.new("AWSTemplateFormatVersion", String, nil),
18
+ "Description" => Section.new("Description", String, 1024),
19
+
20
+ "Conditions" => Section.new("Conditions", Hash, nil),
21
+ "Mappings" => Section.new("Mappings", Hash, 100),
22
+ "Metadata" => Section.new("Metadata", Hash, nil),
23
+ "Outputs" => Section.new("Outputs", Hash, 60),
24
+ "Parameters" => Section.new("Parameters", Hash, 60),
25
+ "Resources" => Section.new("Resources", Hash, nil),
26
+ "Transforms" => Section.new("Transforms", Hash, nil),
27
+ }
28
+ @limit = 51200
29
+
30
+ # Keep track of parsed files to avoid loops
31
+ @parsed = {}
32
+
33
+ # Save a binding so ERB can reuse it instead of creating a new one
34
+ # every time we load a file. This allows ERB code in one file to
35
+ # be referenced in another.
36
+ @binding = binding
37
+
38
+ # See add_domain_types
39
+ @yaml_domain = "mlfs.org,2019"
40
+ @global_tag = "tag:#{@yaml_domain}:"
41
+ end
42
+
43
+ # Shorthand accessor for template sections
44
+ def [](section); @sections[section]; end
45
+
46
+ # Return the length of the entire template
47
+ def length; to_json.to_s.length; end
48
+
49
+ # Check if the template exceeds the AWS limit
50
+ def exceeds_limit?; limit && length > limit; end
51
+
52
+
53
+ #########################
54
+ # Merge contents of a file
55
+ def merge(file, erb)
56
+ contents = load(file, erb)
57
+
58
+ begin
59
+ # Try JSON
60
+ cfn = JSON.load(contents)
61
+ rescue Exception => e
62
+ # Try YAML
63
+ add_domain_types
64
+ cfn = YAML.load(contents)
65
+ end
66
+
67
+ # Merge sections that are present in the file
68
+ @sections.each { |name, section| section.merge(cfn[name]) if cfn[name] }
69
+
70
+ # Look for Includes and merge them
71
+ # Files are Included relative to the file with the Include directive
72
+ cfn["Include"].each do |file|
73
+ Dir.chdir(File.dirname(file)) { self.merge(File.basename(file), erb) }
74
+ end if cfn["Include"]
75
+ end
76
+
77
+
78
+ #########################
79
+ # Call ValidateTemplate[https://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/API_ValidateTemplate.html]
80
+ def validate
81
+ require "aws-sdk-cloudformation"
82
+ Aws::CloudFormation::Client.new.validate_template({ template_body: pp })
83
+ end
84
+
85
+
86
+ #########################
87
+ # Format contents
88
+ def pp
89
+ case @format
90
+ when "json"
91
+ to_hash.to_json
92
+ when "yaml"
93
+ # Strip enclosing quotes around tags and revert tags to their short form
94
+ # And keep Psych from splitting "long" lines
95
+ to_hash.to_yaml({ line_width: -1 }).gsub(/"(#{@global_tag}[^"]+?)"/, '\1').gsub("#{@global_tag}", "!")
96
+ else
97
+ to_hash
98
+ end
99
+ end
100
+
101
+ private
102
+
103
+ #########################
104
+ # Create a hash of each populated section's contents
105
+ def to_hash
106
+ h = Hash.new
107
+ @sections.each { |k, v| h[k] = v.contents if v.length > 0 }
108
+ h
109
+ end
110
+
111
+
112
+ #########################
113
+ # Read file and (optionally) perform ERB processing on it
114
+ def load(file, erb = true)
115
+ path = File.expand_path(file)
116
+ raise KeyError if @parsed.has_key?(path)
117
+
118
+ $logger.info { "Loading #{file}" } if $logger
119
+
120
+ contents = File.read(file)
121
+ contents = ERB.new(contents).result(@binding) if erb
122
+
123
+ @parsed[path] = true
124
+
125
+ return contents
126
+ end
127
+
128
+
129
+ #########################
130
+ # Define YAML domains to handle CloudFormation intrinsic
131
+ # functions.
132
+ #
133
+ # CloudFormation uses <b>!</b> to denote the YAML short form of
134
+ # intrinsic functions, which is the same prefix YAML uses for
135
+ # local tags. The default handler strips undefined local tags,
136
+ # leaving just the value.
137
+ #
138
+ # This puts the tags back, but in global tag format. The global
139
+ # tag prefix is converted back to <b>!</b> on output.
140
+ #
141
+ # Using the short form will force the output to be in YAML format.
142
+ def add_domain_types
143
+ functions = [
144
+ "Base64",
145
+ "Cidr",
146
+ "FindInMap",
147
+ "GetAtt",
148
+ "GetAZs",
149
+ "ImportValue",
150
+ "Join",
151
+ "Ref",
152
+ "Select",
153
+ "Split",
154
+ "Transform",
155
+ "And",
156
+ "Equals",
157
+ "If",
158
+ "Not",
159
+ "Or",
160
+ ].each do |function|
161
+ YAML::add_domain_type(@yaml_domain, function) do |type, val|
162
+ @format = "yaml"
163
+ "#{type} #{val}"
164
+ end
165
+ end
166
+
167
+ # The syntax for !Sub requires double-quotes around Strings
168
+ functions = [
169
+ "Sub",
170
+ ].each do |function|
171
+ YAML::add_domain_type(@yaml_domain, function) do |type, val|
172
+ @format = "yaml"
173
+ val.is_a?(String)? "#{type} \"#{val}\"" : "#{type} #{val}"
174
+ end
175
+ end
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,18 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "mkstack"
3
+ s.version = "1.0.0"
4
+ s.summary = "Merge multiple CloudFormation template files into a single template"
5
+ s.description = <<-EOF
6
+ Merge multiple CloudFormation template files into a single template. Each file may be in either JSON or YAML format. By default all files are run through an ERB (Embedded RuBy) processor.
7
+ EOF
8
+
9
+ s.authors = [ "Andy Rosen" ]
10
+ s.email = [ "ajr@corp.mlfs.org" ]
11
+ s.homepage = "https://github.com/ajrosen/AWS/tree/master/mkstack"
12
+ s.licenses = [ "GPL-3.0+" ]
13
+
14
+ s.files = Dir[ "mkstack.gemspec", "bin/*", "lib/**/*.rb" ]
15
+ s.executables = [ "mkstack" ]
16
+
17
+ s.add_runtime_dependency "aws-sdk-cloudformation", "~> 1"
18
+ end
metadata ADDED
@@ -0,0 +1,94 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mkstack
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Andy Rosen
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain:
11
+ - |
12
+ -----BEGIN CERTIFICATE-----
13
+ MIIEOjCCAqKgAwIBAgIBATANBgkqhkiG9w0BAQsFADAlMSMwIQYDVQQDDBphanIv
14
+ REM9Y29ycC9EQz1tbGZzL0RDPW9yZzAeFw0xOTA1MDEwMjIwNTBaFw0yMDA0MzAw
15
+ MjIwNTBaMCUxIzAhBgNVBAMMGmFqci9EQz1jb3JwL0RDPW1sZnMvREM9b3JnMIIB
16
+ ojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAqQ2xCUJ4wY8WujSzYd3OGTbj
17
+ JDMeU44pXOTLc49Rs8ydukGfd0YBvYMzifmiVRj6depGx2+Ln28Y2mT6IB+zHq8X
18
+ s1lrMdFCReztJjQ7OYS16YcZ6pmLkYClnHN3VNqayk1lQEJGCr8aawMeroSB01om
19
+ d5wqDATnKEG3x4bnlxFJb3LHzUG1CgNuVCuNREi8zN/uYdm2MGe1fTJguy4/vzBQ
20
+ /FnAMt1mr3LtM6YZRGaitIlOKBV/08v7fjH31KRmSMMHPq6A+WyWKRNKnK3tHpSN
21
+ JbnFW7mFQGtBpfh8zY8OCQ76Aw8cb5bEIPTI+Hd4FoJLPKnexTI28endSLigOUCF
22
+ 76kLOiVOVOjdqZ8vEdSgWVugEAzIC30xW5b8yD6N7GdT7n3ktgoZu7jZMUW3D6PQ
23
+ wS6BaABVsXbeVtFrzK2tQ65EwLfRluRLTfbnQ7qvMIYCwC3Ib9DTMLavCiOot1vc
24
+ RBWXIrDex0tt3EN0dDIxP9O+7ciDgM4zPe6BvaF/AgMBAAGjdTBzMAkGA1UdEwQC
25
+ MAAwCwYDVR0PBAQDAgSwMB0GA1UdDgQWBBRDowzw+kmcX6FV7T9BkQsUgmzLZjAc
26
+ BgNVHREEFTATgRFhanJAY29ycC5tbGZzLm9yZzAcBgNVHRIEFTATgRFhanJAY29y
27
+ cC5tbGZzLm9yZzANBgkqhkiG9w0BAQsFAAOCAYEAbbgfVpRCtujGFRHNYLWnq/iQ
28
+ vGtNs265jQKdq3SZ5HIsPP4RdiknOk2Q0BP4GDkXOhadtuuqeCVlJUczcrCKiKuP
29
+ Vu7iOQKpOq9bafhjvTpRPZL7uXDu0lwBrDyL9PGouBBsijTtGCc/A8cu/2HVoX+Z
30
+ X1pqmuJqVlgXp9ktbzPBeIdaFFT+9WzIzWCJ73oBYNZ1EP/4CRKIlmRCWzBGZtu6
31
+ QqNypgW/OCQVRcJGZ99myNQO5/EImQp1py6iHymkdCx2RMsdTUpcm3gUl4XaFd4J
32
+ pQ1HNcDdD3UCSDqlPpv2nVpOV4u1s3yojIHt0RcVXhSzqGHgmXNjKDIP0zokFBKx
33
+ NigI/5A3vv9PplxXMatOeirqPqDY5F4Aim4t2Dn+d+9OiPADkIBwuIuwwgRmcWhE
34
+ E2DV+ftSSpAT1zeHEZXx6BsvJVwtXq1kMDYibgB4lTU4lq1oHrx2x+PO2cukHlom
35
+ 4cVT/heohjMDtNBaDMVsTjTxtNq6oi/pvyLqBGmQ
36
+ -----END CERTIFICATE-----
37
+ date: 2019-05-02 00:00:00.000000000 Z
38
+ dependencies:
39
+ - !ruby/object:Gem::Dependency
40
+ name: aws-sdk-cloudformation
41
+ requirement: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - "~>"
44
+ - !ruby/object:Gem::Version
45
+ version: '1'
46
+ type: :runtime
47
+ prerelease: false
48
+ version_requirements: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - "~>"
51
+ - !ruby/object:Gem::Version
52
+ version: '1'
53
+ description: 'Merge multiple CloudFormation template files into a single template. Each
54
+ file may be in either JSON or YAML format. By default all files are run through
55
+ an ERB (Embedded RuBy) processor.
56
+
57
+ '
58
+ email:
59
+ - ajr@corp.mlfs.org
60
+ executables:
61
+ - mkstack
62
+ extensions: []
63
+ extra_rdoc_files: []
64
+ files:
65
+ - bin/mkstack
66
+ - lib/mkstack.rb
67
+ - lib/mkstack/section.rb
68
+ - lib/mkstack/template.rb
69
+ - mkstack.gemspec
70
+ homepage: https://github.com/ajrosen/AWS/tree/master/mkstack
71
+ licenses:
72
+ - GPL-3.0+
73
+ metadata: {}
74
+ post_install_message:
75
+ rdoc_options: []
76
+ require_paths:
77
+ - lib
78
+ required_ruby_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ required_rubygems_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ requirements: []
89
+ rubyforge_project:
90
+ rubygems_version: 2.7.6
91
+ signing_key:
92
+ specification_version: 4
93
+ summary: Merge multiple CloudFormation template files into a single template
94
+ test_files: []
Binary file