cloud_compose 0.1.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 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: []