murk 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
+ SHA1:
3
+ metadata.gz: 94b5fa2f76a059be9b95513044c349bcff000d0a
4
+ data.tar.gz: 57827970004e4c1ac3e0250165acdf749ce56c2d
5
+ SHA512:
6
+ metadata.gz: dbb5476ead46d143a845eb9c645277eba34492c6dc0f63daacb550a130e58c14429e1897db5a85a303ce5b2d1bb5f6e79037b542c88ed11c484ae41fcbc8e469
7
+ data.tar.gz: 0471a34e8f8c3b19f46f0202b4ef4ccc6e9f6dc8c9c193c4d2f4cb9529c1d6e7eff000cf0b6720ef6b24f4a1a20526c4632b399543f8fbfef1b4779884305538
data/bin/murk ADDED
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
4
+
5
+ require 'logger'
6
+ require 'optparse'
7
+ require 'ostruct'
8
+
9
+ require 'murk'
10
+
11
+ DEFAULT_CONFIG_FILE = './config/murk.rb'
12
+
13
+ def change(options)
14
+ stack_name = ARGV[1]
15
+ stacks = Murk.load(options.file)
16
+ stack = stacks.find_by_name(stack_name, env: ENV['MURK_ENV'] || ENV['USER'])
17
+ if stack
18
+ yield stack
19
+ else
20
+ fail ArgumentError, 'No stack defined with name ' + stack_name
21
+ end
22
+ rescue StandardError => e
23
+ fail_with_exception(e, options.debug)
24
+ end
25
+
26
+ def fail_with_usage(parser)
27
+ $stderr.puts parser
28
+ exit 1
29
+ end
30
+
31
+ def fail_with_exception(e, debug)
32
+ cause = e.cause || e
33
+ $stderr.puts e.message
34
+ $stderr.puts cause.message
35
+ if debug
36
+ $stderr.puts cause.backtrace
37
+ end
38
+ exit 2
39
+ end
40
+
41
+ options = OpenStruct.new
42
+ options.file = DEFAULT_CONFIG_FILE
43
+ options.debug = false
44
+
45
+ parser = OptionParser.new do |p|
46
+ p.banner = 'Usage: stack [options] [ create | delete ] STACK'
47
+
48
+ p.on('-f', '--file FILE', 'Stack configuration file') do |file|
49
+ options.file = file
50
+ end
51
+
52
+ p.on('-d', '--debug', 'Show extra debugging output') do |debug|
53
+ options.debug = debug
54
+ Murk.logger.level = Logger::DEBUG
55
+ end
56
+ end
57
+
58
+ begin
59
+ parser.parse!(ARGV)
60
+ rescue OptionParser::ParseError
61
+ fail_with_usage(parser)
62
+ end
63
+
64
+ fail_with_usage(parser) unless ARGV.length >= 2
65
+
66
+ if ARGV[0] == 'create'
67
+ change(options) { |stack| stack.create_or_update }
68
+ elsif ARGV[0] == 'delete'
69
+ change(options) { |stack| stack.delete }
70
+ else
71
+ fail_with_usage(parser)
72
+ end
73
+
74
+ exit 0
data/lib/murk/aws.rb ADDED
@@ -0,0 +1,11 @@
1
+ require 'aws-sdk'
2
+
3
+ module Murk
4
+ module AWS
5
+
6
+ def cloudformation
7
+ @cloudformation ||= Aws::CloudFormation::Client.new
8
+ end
9
+
10
+ end
11
+ end
@@ -0,0 +1,45 @@
1
+
2
+ module Murk
3
+ module Builder
4
+
5
+ class MurkBuilder
6
+
7
+ def initialize
8
+ @options_builder = OptionsBuilder.new
9
+ @stack_builders = []
10
+ @current_env = nil
11
+ end
12
+
13
+ def options(&block)
14
+ @options_builder.instance_eval(&block)
15
+ self
16
+ end
17
+
18
+ def env(name, &block)
19
+ @current_env = name
20
+ instance_eval(&block)
21
+ @current_env = nil
22
+ self
23
+ end
24
+
25
+ def stack(name, &block)
26
+ stack_builder = StackBuilder.new(name, env: @current_env)
27
+ stack_builder.instance_eval(&block)
28
+ @stack_builders << stack_builder
29
+ self
30
+ end
31
+
32
+ def build
33
+ Murk.configure(@options_builder.build)
34
+
35
+ stack_collection = Murk::Model::StackCollection.new
36
+ @stack_builders.each do |builder|
37
+ stack_collection.add(builder.build)
38
+ end
39
+ stack_collection
40
+ end
41
+
42
+ end
43
+
44
+ end
45
+ end
@@ -0,0 +1,26 @@
1
+
2
+ module Murk
3
+ module Builder
4
+
5
+ class OptionsBuilder
6
+
7
+ def initialize
8
+ @options = {}
9
+ end
10
+
11
+ def build
12
+ @options
13
+ end
14
+
15
+ def respond_to?(_method_sym)
16
+ true
17
+ end
18
+
19
+ def method_missing(method_sym, *args)
20
+ @options[method_sym] = args[0]
21
+ end
22
+
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,31 @@
1
+
2
+ require 'murk/model/stack_parameter'
3
+
4
+ module Murk
5
+ module Builder
6
+
7
+ class ParametersBuilder
8
+
9
+ def initialize
10
+ @parameters = []
11
+ end
12
+
13
+ def build
14
+ @parameters
15
+ end
16
+
17
+ def respond_to?(_method_sym)
18
+ true
19
+ end
20
+
21
+ def method_missing(method_sym, *args, &block)
22
+ if args.length > 0
23
+ @parameters << Murk::Model::SimpleStackParameter.new(method_sym, args[0])
24
+ else
25
+ @parameters << Murk::Model::ReferenceStackParameter.new(method_sym, block)
26
+ end
27
+ end
28
+
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,32 @@
1
+
2
+ module Murk
3
+ module Builder
4
+
5
+ class StackBuilder
6
+
7
+ def initialize(stack_name, env: nil)
8
+ @stack_name = stack_name
9
+ @env = env
10
+ @parameters_builder = ParametersBuilder.new
11
+ end
12
+
13
+ def build
14
+ stack = Murk::Model::Stack.new(@stack_name, env: @env)
15
+ @parameters_builder.build.each do |parameter|
16
+ stack.add_parameter(parameter)
17
+ end
18
+ stack
19
+ end
20
+
21
+ def parameters(&block)
22
+ @parameters_builder.instance_eval(&block)
23
+ self
24
+ end
25
+
26
+ end
27
+
28
+ class ConfigError < StandardError
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,126 @@
1
+
2
+ require 'aws-sdk'
3
+ require 'murk/model/template'
4
+
5
+ module Murk
6
+ module Model
7
+ class Stack
8
+
9
+ include Murk
10
+ include Murk::AWS
11
+
12
+ attr_reader :name, :env
13
+ attr_accessor :collection
14
+
15
+ def initialize(name, env: nil, template_filename: name + '.json')
16
+ @name = name
17
+ @env = env
18
+ @template = Template.new(template_filename)
19
+ @parameters = []
20
+ end
21
+
22
+ def add_parameter(parameter)
23
+ if @template.parameter?(parameter.key)
24
+ @parameters << parameter
25
+ else
26
+ fail StackError, "No such parameter '#{parameter.key}' for template '#{@template.filename}'"
27
+ end
28
+ end
29
+
30
+ def parameter_value(parameter_key)
31
+ @parameters.find { |parameter| parameter.key == parameter_key }.resolve(@collection)
32
+ end
33
+
34
+ def create_or_update
35
+ fail StackError, "Stack '#{@name}' is in failed state" if failed?
36
+ exists? ? update : create
37
+ end
38
+
39
+ def delete
40
+ fail StackError "Stack #{@name} does not exist" unless exists?
41
+ cloudformation.delete_stack(stack_name: qualified_name)
42
+ end
43
+
44
+ def exists?
45
+ existing.any?
46
+ end
47
+
48
+ def failed?
49
+ existing.any? do |stack|
50
+ stack.stack_status =~ /FAILED/
51
+ end
52
+ end
53
+
54
+ def qualified_name
55
+ qualified_name = ''
56
+ if Murk.options[:stack_prefix]
57
+ qualified_name += Murk.options[:stack_prefix] + '-'
58
+ end
59
+ if @env
60
+ qualified_name += @env + '-'
61
+ end
62
+ qualified_name + @name
63
+ end
64
+
65
+ def output(key)
66
+ return unless exists?
67
+ outputs = cloudformation.describe_stacks(stack_name: qualified_name)[:stacks][0][:outputs]
68
+ output = outputs.find { |o| o.output_key == key.to_s }
69
+ output ? output.output_value : nil
70
+ end
71
+
72
+ private
73
+
74
+ def create
75
+ cloudformation.create_stack(config)
76
+ rescue Aws::CloudFormation::Errors::ValidationError
77
+ raise StackError, "Failed to create stack #{@name}"
78
+ end
79
+
80
+ def update
81
+ cloudformation.update_stack(config)
82
+ rescue Aws::CloudFormation::Errors::ValidationError
83
+ if e.message =~ /No updates are to be performed/
84
+ return
85
+ else
86
+ raise StackError, "Failed to update stack #{@name}"
87
+ end
88
+ end
89
+
90
+ def existing
91
+ cloudformation.list_stacks.stack_summaries.select do |stack|
92
+ stack.stack_name == qualified_name && stack.stack_status != 'DELETE_COMPLETE'
93
+ end
94
+ end
95
+
96
+ def config
97
+ {
98
+ stack_name: qualified_name,
99
+ template_body: @template.body,
100
+ capabilities: ['CAPABILITY_IAM'],
101
+ parameters: implicit_parameters + explicit_parameters
102
+ }
103
+ end
104
+
105
+ def implicit_parameters
106
+ implicit_parameters = {}
107
+ implicit_parameters[:Prefix] = Murk.options[:stack_prefix] if @template.parameter?(:Prefix)
108
+ implicit_parameters[:Env] = @env if @template.parameter?(:Env)
109
+ implicit_parameters[:Name] = @name if @template.parameter?(:Name)
110
+ implicit_parameters[:QualifiedName] = qualified_name if @template.parameter?(:QualifiedName)
111
+ implicit_parameters.map { |key, value| { parameter_key: key, parameter_value: value } }
112
+ end
113
+
114
+ def explicit_parameters
115
+ @parameters.map do |parameter|
116
+ { parameter_key: parameter.key, parameter_value: parameter.resolve(@collection) }
117
+ end
118
+ end
119
+
120
+ end
121
+
122
+ class StackError < StandardError
123
+ end
124
+
125
+ end
126
+ end
@@ -0,0 +1,37 @@
1
+
2
+ module Murk
3
+ module Model
4
+ class StackCollection
5
+
6
+ include Enumerable
7
+
8
+ def initialize
9
+ @stacks = []
10
+ end
11
+
12
+ def add(stack)
13
+ @stacks << stack
14
+ stack.collection = self
15
+ end
16
+
17
+ def each(&block)
18
+ @stacks.each(&block)
19
+ end
20
+
21
+ def find_by_name(name, env: nil)
22
+ find do |stack|
23
+ stack.name == name && stack.env == env
24
+ end
25
+ end
26
+
27
+ def respond_to?(method_sym)
28
+ @stacks.any? { |stack| stack.name == method_sym.to_s }
29
+ end
30
+
31
+ def method_missing(method_sym)
32
+ @stacks.find { |stack| stack.name == method_sym.to_s }
33
+ end
34
+
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,39 @@
1
+
2
+ module Murk
3
+ module Model
4
+
5
+ class SimpleStackParameter
6
+
7
+ attr_reader :key, :value
8
+
9
+ def initialize(key, value)
10
+ @key = key
11
+ @value = value
12
+ end
13
+
14
+ def resolve(stack_collection)
15
+ @value
16
+ end
17
+
18
+ def ==(other)
19
+ @key == other.key && @value == other.value
20
+ end
21
+
22
+ end
23
+
24
+ class ReferenceStackParameter
25
+
26
+ attr_reader :key, :block
27
+
28
+ def initialize(key, block)
29
+ @key = key
30
+ @block = block
31
+ end
32
+
33
+ def resolve(stack_collection)
34
+ stack_collection.instance_eval(&@block)
35
+ end
36
+
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,58 @@
1
+
2
+ require 'aws-sdk'
3
+
4
+ module Murk
5
+ module Model
6
+ class Template
7
+
8
+ include Murk
9
+ include Murk::AWS
10
+
11
+ attr_reader :filename
12
+
13
+ def initialize(filename)
14
+ @filename = filename
15
+ end
16
+
17
+ def path
18
+ @path ||= resolve_path(@filename)
19
+ end
20
+
21
+ def body
22
+ File.read(path)
23
+ end
24
+
25
+ def parameters
26
+ validate.parameters
27
+ end
28
+
29
+ def parameter?(parameter_key)
30
+ parameters.any? { |param| param.parameter_key == parameter_key.to_s }
31
+ end
32
+
33
+ private
34
+
35
+ def resolve_path(filename)
36
+ template_paths = Murk.options[:template_path].split(':')
37
+ template_paths.each do |path|
38
+ real_path = File.absolute_path(path, Murk.config_dir)
39
+ if File.exist?(File.join(real_path, filename))
40
+ return File.join(real_path, filename)
41
+ end
42
+ end
43
+ fail TemplateError, "Template '#{filename}' not found in path #{Murk.options[:template_path]}"
44
+ end
45
+
46
+ def validate
47
+ @validate_output ||= cloudformation.validate_template(template_body: body)
48
+ rescue Aws::CloudFormation::Errors::ValidationError
49
+ raise TemplateError, "Failed to validate template at #{path}"
50
+ end
51
+
52
+ end
53
+
54
+ class TemplateError < StandardError
55
+ end
56
+
57
+ end
58
+ end
data/lib/murk.rb ADDED
@@ -0,0 +1,60 @@
1
+
2
+ require 'logger'
3
+
4
+ module Murk
5
+
6
+ DEFAULT_OPTIONS = {
7
+ template_path: ENV['MURK_PATH'] || '',
8
+ stack_prefix: ENV['MURK_PREFIX']
9
+ }
10
+
11
+ def self.configure(options = {})
12
+ @options = self.options.merge(options)
13
+ end
14
+
15
+ def self.options
16
+ @options ||= DEFAULT_OPTIONS
17
+ end
18
+
19
+ def self.load(file)
20
+ self.config_file = file
21
+ config = File.read(config_file)
22
+ builder = Murk::Builder::MurkBuilder.new
23
+ builder.instance_eval(config)
24
+ builder.build
25
+ end
26
+
27
+ # rubocop:disable Style/ClassVars
28
+ def self.logger
29
+ @@logger ||= Logger.new(STDOUT).tap do |log|
30
+ log.level = Logger::INFO
31
+ log.progname = 'Murk'
32
+ end
33
+ end
34
+
35
+ def self.logger=(logger)
36
+ @@logger = logger.tap do |log|
37
+ log.progname = 'Murk'
38
+ end
39
+ end
40
+
41
+ def self.config_file
42
+ @@config_file ||= File.absolute_path('./config')
43
+ end
44
+
45
+ def self.config_file=(config_file)
46
+ @@config_file = config_file
47
+ end
48
+ # rubocop:ensable Style/ClassVars
49
+
50
+ def self.config_dir
51
+ File.dirname(config_file)
52
+ end
53
+
54
+ def logger
55
+ Murk.logger
56
+ end
57
+
58
+ end
59
+
60
+ Dir.glob(File.join(File.dirname(__FILE__), 'murk', '**/*.rb')).each { |file| require file }
metadata ADDED
@@ -0,0 +1,153 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: murk
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Cam Smith
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-08-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: api_cache
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.3'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: aws-sdk
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.1'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: json
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.8'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.8'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.10'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.10'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rubocop
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.32'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.32'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '3.3'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '3.3'
97
+ - !ruby/object:Gem::Dependency
98
+ name: simplecov
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '0.10'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '0.10'
111
+ description: Murk - Parameterization and environmental partitioning for AWS CloudFormation
112
+ email:
113
+ executables:
114
+ - murk
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - bin/murk
119
+ - lib/murk.rb
120
+ - lib/murk/aws.rb
121
+ - lib/murk/builder/murk_builder.rb
122
+ - lib/murk/builder/options_builder.rb
123
+ - lib/murk/builder/parameters_builder.rb
124
+ - lib/murk/builder/stack_builder.rb
125
+ - lib/murk/model/stack.rb
126
+ - lib/murk/model/stack_collection.rb
127
+ - lib/murk/model/stack_parameter.rb
128
+ - lib/murk/model/template.rb
129
+ homepage: https://github.com/snoremac/murk
130
+ licenses:
131
+ - MIT
132
+ metadata: {}
133
+ post_install_message:
134
+ rdoc_options: []
135
+ require_paths:
136
+ - lib
137
+ required_ruby_version: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - ">="
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ required_rubygems_version: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ requirements: []
148
+ rubyforge_project:
149
+ rubygems_version: 2.4.8
150
+ signing_key:
151
+ specification_version: 4
152
+ summary: Murk
153
+ test_files: []