cloud_compose 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8d01e1a9806a945e7b31002083f917ce6879392eb6238d0e26558a81f567b9b4
4
+ data.tar.gz: 004dac72cc30a68a8cd21bdc4be8b11aa7e28c7b6f9a45b9bfd83e94043008cc
5
+ SHA512:
6
+ metadata.gz: 9a60c29f02ec15e8297b1c46a72a1a6b0c95cf6756f48dfa99929fd67bba81588cd5fc7d0bd8e1c2117c8a7eefbbc79b57234785af301908c274d9ca57d40a23
7
+ data.tar.gz: d585de3c2d5eccd3fef396a54701457021beb05422042d4f5facd1f51ed151b63c946bf817f1b1872ac4adc0fbc63b21b990fc1dc64ecf70708bd4d8229caf45
data/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # CloudCompose
2
+
3
+ Compose multiple cloud formation templates into one file.
4
+
5
+ `cloud-compose ./template.yml ./output`
6
+
7
+ ```yaml
8
+ # template.yml
9
+ ---
10
+ $cloud_compose:
11
+ parameters:
12
+ GlobalName: 'TestTemplate'
13
+ imports:
14
+ - name: SubTemplateOne
15
+ path: ./sub_readme.yml
16
+ - name: SubTemplateTwo
17
+ path: ./sub_readme.yml
18
+ ---
19
+
20
+ Resources:
21
+ $(GlobalName)MainResource:
22
+ Type: AWS::Fake::Thing
23
+ Properties:
24
+ Name: My Main Resource
25
+ SecondaryResource:
26
+ Type: AWS::Fake::Thing
27
+ Properties:
28
+ ParentThing: !Ref $(GlobalName)MainResource
29
+ ```
30
+
31
+ ```yaml
32
+ # sub_readme.yml
33
+ ---
34
+ $cloud_compose:
35
+ partial: true
36
+ ---
37
+
38
+ Resources:
39
+ $(name)Resource:
40
+ Type: AWS::Fake::Thing
41
+ Properties:
42
+ ParentThing: !Ref $(GlobalName)MainResource
43
+ Outputs:
44
+ $(name)ResourceOutput:
45
+ Value: !Ref $(name)Resource
46
+
47
+ ```
48
+
49
+ ```yaml
50
+ # Output
51
+ ---
52
+ Resources:
53
+ TestTemplateMainResource:
54
+ Type: AWS::Fake::Thing
55
+ Properties:
56
+ Name: My Main Resource
57
+ SecondaryResource:
58
+ Type: AWS::Fake::Thing
59
+ Properties:
60
+ ParentThing: !Ref TestTemplateMainResource
61
+ SubTemplateOneResource:
62
+ Type: AWS::Fake::Thing
63
+ Properties:
64
+ ParentThing: !Ref TestTemplateMainResource
65
+ SubTemplateTwoResource:
66
+ Type: AWS::Fake::Thing
67
+ Properties:
68
+ ParentThing: !Ref TestTemplateMainResource
69
+ Outputs:
70
+ SubTemplateOneResourceOutput:
71
+ Value: !Ref SubTemplateOneResource
72
+ SubTemplateTwoResourceOutput:
73
+ Value: !Ref SubTemplateTwoResource
74
+ ```
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require 'bundler/gem_tasks'
2
+ task default: :spec
@@ -0,0 +1,41 @@
1
+ require_relative 'parser'
2
+
3
+ module CloudCompose
4
+ class Config
5
+ Import = Struct.new(:name, :path, :parameters)
6
+
7
+ def initialize(config, root)
8
+ @root = root
9
+ params = CloudCompose::Parser.load_yaml(config)
10
+ @config = params.fetch('$cloud_compose')
11
+ end
12
+
13
+ def partial?
14
+ @config.fetch('partial', false) == true
15
+ end
16
+
17
+ def imports
18
+ @config.fetch('imports', []).map do |obj|
19
+ Import.new(
20
+ obj.fetch('name'),
21
+ import_path(obj),
22
+ obj.fetch('parameters', {})
23
+ )
24
+ end
25
+ end
26
+
27
+ def parameters
28
+ @config.fetch('parameters', {})
29
+ end
30
+
31
+ def required_parameters
32
+ @config.fetch('require', [])
33
+ end
34
+
35
+ private
36
+
37
+ def import_path(obj)
38
+ File.expand_path(obj.fetch('path'), @root)
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,45 @@
1
+ require 'psych'
2
+
3
+ require_relative 'tags/simple_type'
4
+
5
+ require_relative 'tags/get_att'
6
+ require_relative 'tags/sub'
7
+
8
+ module CloudCompose
9
+ class Parser
10
+ CUSTOM_TAGS = {
11
+ '!And' => CloudCompose::Tags::And,
12
+ '!Base64' => CloudCompose::Tags::Base64,
13
+ '!Cidr' => CloudCompose::Tags::Cidr,
14
+ '!Condition' => CloudCompose::Tags::Condition,
15
+ '!Equals' => CloudCompose::Tags::Equals,
16
+ '!FindInMap' => CloudCompose::Tags::FindInMap,
17
+ '!GetAZs' => CloudCompose::Tags::GetAzs,
18
+ '!GetAtt' => CloudCompose::Tags::GetAtt,
19
+ '!If' => CloudCompose::Tags::If,
20
+ '!ImportValue' => CloudCompose::Tags::ImportValue,
21
+ '!Join' => CloudCompose::Tags::Join,
22
+ '!Not' => CloudCompose::Tags::Not,
23
+ '!Or' => CloudCompose::Tags::Or,
24
+ '!Ref' => CloudCompose::Tags::Ref,
25
+ '!Select' => CloudCompose::Tags::Select,
26
+ '!Split' => CloudCompose::Tags::Split,
27
+ '!Sub' => CloudCompose::Tags::Sub,
28
+ '!Transform' => CloudCompose::Tags::Transform
29
+ }.freeze
30
+
31
+ class << self
32
+ def load_yaml(content)
33
+ Psych.safe_load(content, CUSTOM_TAGS.values)
34
+ end
35
+
36
+ def dump_yaml(object)
37
+ Psych.dump(object)
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ CloudCompose::Parser::CUSTOM_TAGS.each do |key, klass|
44
+ Psych.add_tag(key, klass)
45
+ end
@@ -0,0 +1,15 @@
1
+ module CloudCompose
2
+ class Processor
3
+ PRE_PROCESSOR_PARAMETER_REGEXP = Regexp.new('\$\((?:[a-zA-Z0-9_]+)\)?').freeze
4
+ PRE_PROCESSOR_PARAMETER_NAME_REGEXP = Regexp.new('\A\$\((?<token>[a-zA-Z0-9_]+)\)?\z').freeze
5
+
6
+ class << self
7
+ def preprocess(content, context)
8
+ content.gsub(PRE_PROCESSOR_PARAMETER_REGEXP) do |matched|
9
+ key = PRE_PROCESSOR_PARAMETER_NAME_REGEXP.match(matched)['token']
10
+ context.fetch(key)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,46 @@
1
+ module CloudCompose
2
+ module Tags
3
+ class InvalidTypeError < StandardError
4
+ attr_reader :tag
5
+ attr_reader :type
6
+
7
+ def initialize(tag, type)
8
+ @tag = tag
9
+ @type = type
10
+ super("Invalid Type (#{type}) for tag `#{tag}`")
11
+ end
12
+ end
13
+
14
+ class Base
15
+ attr_reader :type
16
+
17
+ attr_reader :value
18
+
19
+ def type_from_coder(coder)
20
+ coder.type
21
+ end
22
+
23
+ def value_from_coder(_coder)
24
+ raise NotImplementedError, "Missing #{__method__} override."
25
+ end
26
+
27
+ def init_with(coder)
28
+ initialize(
29
+ type_from_coder(coder),
30
+ value_from_coder(coder)
31
+ )
32
+ end
33
+
34
+ def encode_with(coder)
35
+ coder.send("#{type.to_sym}=", value)
36
+ end
37
+
38
+ private
39
+
40
+ def initialize(type, value)
41
+ @type = type
42
+ @value = value
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,22 @@
1
+ require_relative 'base'
2
+
3
+ module CloudCompose
4
+ module Tags
5
+ class GetAtt < Base
6
+ def type_from_coder(_coder)
7
+ :seq
8
+ end
9
+
10
+ def value_from_coder(coder)
11
+ case coder.type
12
+ when :seq
13
+ coder.seq
14
+ when :scalar
15
+ coder.scalar.split('.', 2)
16
+ else
17
+ raise InvalidTypeError.new(coder.tag, coder.type)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,16 @@
1
+ require_relative 'base'
2
+
3
+ module CloudCompose
4
+ module Tags
5
+ class MapType < Base
6
+ def value_from_coder(coder)
7
+ case coder.type
8
+ when :map
9
+ coder.map
10
+ else
11
+ raise InvalidTypeError.new(coder.tag, coder.type)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ require_relative 'base'
2
+
3
+ module CloudCompose
4
+ module Tags
5
+ class ScalarType < Base
6
+ def value_from_coder(coder)
7
+ case coder.type
8
+ when :scalar
9
+ coder.scalar
10
+ else
11
+ raise InvalidTypeError.new(coder.tag, coder.type)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ require_relative 'base'
2
+
3
+ module CloudCompose
4
+ module Tags
5
+ class SequencedType < Base
6
+ def value_from_coder(coder)
7
+ case coder.type
8
+ when :seq
9
+ coder.seq
10
+ else
11
+ raise InvalidTypeError.new(coder.tag, coder.type)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,24 @@
1
+ require_relative 'map_type'
2
+ require_relative 'scalar_type'
3
+ require_relative 'sequenced_type'
4
+
5
+ module CloudCompose
6
+ module Tags
7
+ class And < SequencedType; end
8
+ class Base64 < ScalarType; end
9
+ class Cidr < SequencedType; end
10
+ class Condition < ScalarType; end
11
+ class Equals < SequencedType; end
12
+ class FindInMap < SequencedType; end
13
+ class GetAzs < ScalarType; end
14
+ class If < SequencedType; end
15
+ class ImportValue < ScalarType; end
16
+ class Join < SequencedType; end
17
+ class Not < SequencedType; end
18
+ class Or < SequencedType; end
19
+ class Ref < ScalarType; end
20
+ class Select < SequencedType; end
21
+ class Split < SequencedType; end
22
+ class Transform < MapType; end
23
+ end
24
+ end
@@ -0,0 +1,23 @@
1
+ require_relative 'base'
2
+
3
+ module CloudCompose
4
+ module Tags
5
+ class Sub < Base
6
+ def type_from_coder(_coder)
7
+ :seq
8
+ end
9
+
10
+ def value_from_coder(coder)
11
+ case coder.type
12
+ when :seq
13
+ seq = coder.seq.dup
14
+ seq.fill({}, seq.length...2)
15
+ when :scalar
16
+ [coder.scalar, {}]
17
+ else
18
+ raise InvalidTypeError.new(coder.tag, coder.type)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,172 @@
1
+ require 'pathname'
2
+ require 'time'
3
+ require 'digest'
4
+
5
+ require_relative 'parser'
6
+ require_relative 'config'
7
+ require_relative 'processor'
8
+
9
+ module CloudCompose
10
+ class Template
11
+ IMPORT_MERGE_KEYS = %w[
12
+ Metadata
13
+ Parameters
14
+ Mappings
15
+ Conditions
16
+ Transform
17
+ Resources
18
+ Outputs
19
+ ].freeze
20
+
21
+ class Error < StandardError; end
22
+
23
+ Partial = Struct.new(:name, :template, :parameters) do
24
+ def context
25
+ Hash(parameters).merge('name' => name)
26
+ end
27
+ end
28
+
29
+ attr_reader :root, :config, :content, :file_name
30
+
31
+ def initialize(path, pwd)
32
+ @pwd = pwd
33
+ @file_name = File.basename(path)
34
+ @root = Pathname.new(File.dirname(path))
35
+ content = File.read(path)
36
+ @checksum = Digest::SHA256.hexdigest(content)
37
+ parts = content.split(/^---/, 3)
38
+ @config = CloudCompose::Config.new(parts[1], @root)
39
+ @content = parts[2]
40
+ end
41
+
42
+ def to_h
43
+ template_body = create_hash(parameters)
44
+ imported.each do |imp|
45
+ formatted_imp = imp.template.send(:create_hash, imp.context)
46
+ merge_imported(template_body, formatted_imp, imp.name)
47
+ end
48
+
49
+ template_body.keys.each do |key|
50
+ template_body.delete(key) if template_body[key].empty?
51
+ end
52
+
53
+ template_body
54
+ end
55
+
56
+ def to_s
57
+ yaml = CloudCompose::Parser.dump_yaml(to_h)
58
+
59
+ <<~EOF
60
+ # This Template was generated on #{Time.now.utc.iso8601} by CloudCompose #{CloudCompose::VERSION}
61
+ #
62
+ # Structure
63
+ #{description(0)}
64
+ #
65
+ # Integrity
66
+ #{checksum_print}
67
+ #
68
+ #{yaml}
69
+ EOF
70
+ end
71
+
72
+ def imported
73
+ preload_imports!
74
+ @imported
75
+ end
76
+
77
+ def parameters
78
+ @config.parameters
79
+ end
80
+
81
+ def required_parameters
82
+ @config.required_parameters
83
+ end
84
+
85
+ def description(depth)
86
+ callouts = depth.zero? ? '├─' : '↳'
87
+ leading = depth.zero? ? '' : '|'
88
+ padding = ' ' * depth
89
+ joined = imported.map { |i| i.template.description(depth + 2) }
90
+ [
91
+ "# #{leading}#{padding}#{callouts} #{working_path}/#{file_name}",
92
+ *joined
93
+ ].join("\n")
94
+ end
95
+
96
+ def checksums
97
+ return @checksums if @checksums
98
+
99
+ checksums = {}
100
+ checksums["#{working_path}/#{file_name}"] = @checksum
101
+ imported.each do |imp|
102
+ imp.template.checksums.each do |key, value|
103
+ checksums[key] = value
104
+ end
105
+ end
106
+ @checksums = checksums
107
+ @checksums
108
+ end
109
+
110
+ private
111
+
112
+ def checksum_print
113
+ checksums.keys.sort.map do |key|
114
+ "# #{checksums[key]} ✓ #{key}"
115
+ end.join("\n")
116
+ end
117
+
118
+ def working_path
119
+ root.to_s.gsub("#{@pwd}/", '')
120
+ end
121
+
122
+ def merge_imported(body, imported, name)
123
+ IMPORT_MERGE_KEYS.each do |merge_key|
124
+ current = body.fetch(merge_key, {})
125
+ imported_current = imported.fetch(merge_key, {})
126
+ imported_current.keys.each do |merging|
127
+ next unless current.key?(merging)
128
+
129
+ raise Error, "Template #{file_name} already contains a key #{merge_key}.#{merging}. Merging from #{name}"
130
+ end
131
+ body[merge_key] = current.merge(imported_current)
132
+ end
133
+ end
134
+
135
+ def create_hash(context)
136
+ CloudCompose::Parser.load_yaml(
137
+ CloudCompose::Processor.preprocess(content, context)
138
+ )
139
+ end
140
+
141
+ def preload_imports!
142
+ return if defined?(@imported)
143
+
144
+ @imported = @config.imports.map do |imported|
145
+ create_template(imported)
146
+ end
147
+ end
148
+
149
+ def validate_parameters!(imported, full_params, required)
150
+ required.each do |key|
151
+ next if full_params.key?(key)
152
+
153
+ raise Error, "Missing Required Parameter #{key} in #{imported.name}"
154
+ end
155
+ end
156
+
157
+ def create_template(imported)
158
+ template = CloudCompose::Template.new(imported.path, @pwd)
159
+ raise Error, "Template #{File.basename(imported.path)} imported from #{@file_name} is not a partial" unless template.config.partial?
160
+
161
+ full_params = parameters.merge(imported.parameters).merge(template.parameters)
162
+
163
+ validate_parameters!(imported, full_params, template.required_parameters)
164
+
165
+ Partial.new(
166
+ imported.name,
167
+ template,
168
+ full_params
169
+ )
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,3 @@
1
+ module CloudCompose
2
+ VERSION = '0.1.0'.freeze
3
+ end
@@ -0,0 +1,5 @@
1
+ require 'cloud_compose/template'
2
+ require 'cloud_compose/version'
3
+
4
+ module CloudCompose
5
+ end
metadata ADDED
@@ -0,0 +1,142 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cloud_compose
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Maddie Schipper
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2019-03-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: pry
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.12'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.12'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.8'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.8'
69
+ - !ruby/object:Gem::Dependency
70
+ name: simplecov
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.16'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.16'
83
+ - !ruby/object:Gem::Dependency
84
+ name: yard
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '0.9'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '0.9'
97
+ description: ''
98
+ email:
99
+ - me@maddiesch.com
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - README.md
105
+ - Rakefile
106
+ - lib/cloud_compose.rb
107
+ - lib/cloud_compose/config.rb
108
+ - lib/cloud_compose/parser.rb
109
+ - lib/cloud_compose/processor.rb
110
+ - lib/cloud_compose/tags/base.rb
111
+ - lib/cloud_compose/tags/get_att.rb
112
+ - lib/cloud_compose/tags/map_type.rb
113
+ - lib/cloud_compose/tags/scalar_type.rb
114
+ - lib/cloud_compose/tags/sequenced_type.rb
115
+ - lib/cloud_compose/tags/simple_type.rb
116
+ - lib/cloud_compose/tags/sub.rb
117
+ - lib/cloud_compose/template.rb
118
+ - lib/cloud_compose/version.rb
119
+ homepage: ''
120
+ licenses:
121
+ - MIT
122
+ metadata: {}
123
+ post_install_message:
124
+ rdoc_options: []
125
+ require_paths:
126
+ - lib
127
+ required_ruby_version: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ required_rubygems_version: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - ">="
135
+ - !ruby/object:Gem::Version
136
+ version: '0'
137
+ requirements: []
138
+ rubygems_version: 3.0.3
139
+ signing_key:
140
+ specification_version: 4
141
+ summary: ''
142
+ test_files: []