terraframe 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 69c298bb5fae6f55bd0926b9f6ee7d51317a932c
4
+ data.tar.gz: 1e27ef8d075df5f45a8f32b0bc6790b303c575d3
5
+ SHA512:
6
+ metadata.gz: 494647a04c86977f4652244a39984e201693628303619122bd50584a73e80c19c350b0e3c6df1392318de2de5829e8247f114e7b83bffd3dcb652986468b1da2
7
+ data.tar.gz: 4a5b5829ce32c10cf1f39e4e72b77246ada761a612ec5fe2da3f86fe5fa153e532f42d152c49bb1984639fe11839b3ea56f68ccd7fe0067e32dd2c21d140ea92
@@ -0,0 +1,15 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
15
+ *.gem
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in terraframe.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Ed Ropple
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,60 @@
1
+ # Terraframe #
2
+ Terraframe is a processor for a domain-specific language that outputs to the Terraform specification format.
3
+
4
+ [Terraform][1] is a cool infrastructure-as-code system with a boatload of functionality and a really awesome core that makes building out entire clouds on AWS super easy and super fun. I am a fan.
5
+
6
+ _But_. But but but. It's not perfect. And personally, ever since HashiCorp went away from Ruby DSLs as configuration, I have been sad. And worse than sad, I have been unproductive as all hell. I'm just not satisfied with the state of the Terraform description language. It's hard to write, its built-in assumptions are gross (the handling of variables is a minor crime in progress), and it's just a huge step back from being able to deploy, say, a Vagrant instance--or, as I have done, [a configurable number of Vagrant instances][2]--in a nice, comprehensible manner.
7
+
8
+ The configuration, while not CloudFormation-bad, is pretty bad, and I came close to ditching Terraform because it was really hard to write. I got halfway through some ERB templating monstrosity before I learned of a better way: Terraform supports JSON as a declaration notation, and that makes it really easy to build an external DSL that can be exported for use in Terraform.
9
+
10
+ ## Installation ##
11
+ Terraframe is distributed as a RubyGem.
12
+
13
+ ```bash
14
+ gem install terraframe
15
+ ```
16
+
17
+ ## Usage ##
18
+ 1. You'll need to have [Terraform][1] installed to actually use the output of Terraframe.
19
+ 2. Check `terraframe --help` for exhaustive usage details.
20
+
21
+ ## Syntax ##
22
+ For the most part, the Terraframe syntax directly parallels the Terraform syntax, but has been Rubified. At present, most of this is a wrapper around `method_missing`, and so it's a _little_ rocky when dealing with nested data types.
23
+
24
+ You can easily wire up outputs via `id_of(resource_type, resource_name)` and `output_of(resource_type, resource_name, output_name)`, to make things a little less crazy.
25
+
26
+ Variables can be passed in via the `-v` flag in as many YAML files (which will be deep-merged) as you would like. They are exposed to scripts via the `vars` hash.
27
+
28
+ Examples: TBD. To get you started, here's the supported invocations:
29
+
30
+ - `provider :provider_type {}`
31
+ - `resource :resource_type, "resource_name" {}`
32
+ - `resource_type "resource_name {}`
33
+
34
+ The latter deserves special attention, because any attempted invocation other than `provider`, `variable`, `resource`, or `provisioner` will be interpreted as a resource. (`resource :resource_type, "resource_name"` is supported for people who like the Terraform syntax.)
35
+
36
+ ## Future Work ##
37
+ Right now, Terraframe is being extended as I need it. Pull requests very gratefully accepted for
38
+
39
+ - Variable interpolation in YAML variable files (or possibly Ruby variable files that must emit a hash, a la Chef). This will be soon, I'll need it.
40
+ - Supporting `variable` and `provisioner` blocks, neither are hard but they have to get done.
41
+ - Script linting (checking for id existence, only allowing valid keys, etc.) before starting up Terraform. Very low priority, as that's what Terraform itself does.
42
+ - Test coverage.
43
+
44
+ ## Contributing ##
45
+ 1. Fork it ( https://github.com/eropple/terraframe/fork )
46
+ 2. Create your feature branch under the `feature/` prefix, as with [git-flow][3] (`git flow feature start my-new-feature`)
47
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
48
+ 4. Push to the branch (`git push origin feature/my-new-feature`). Please don't change the version number and please make README changes in a separate commit to make everything comprehensible for me.
49
+ 5. Make a pull request!
50
+
51
+ ## Thanks ##
52
+ - My employer, [Leaf][4], for encouraging the development and use of Terraframe. ([We're hiring!][5])
53
+ - William Lee, my platform engineering co-conspirator at Leaf.
54
+ - HashiCorp, 'cause Terraform is fundamentally an awesome project and we're all better because you guys made this.
55
+
56
+ [1]: https://www.terraform.io
57
+ [2]: http://edcanhack.com/2014/07/a-virtual-mesos-cluster-with-vagrant-and-chef/
58
+ [3]: https://www.atlassian.com/git/tutorials/comparing-workflows/feature-branch-workflow
59
+ [4]: http://engineering.leaf.me
60
+ [5]: mailto:eropple+hiring@leaf.me
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,32 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ lib = File.expand_path(File.dirname(__FILE__) + '/../lib')
4
+ $LOAD_PATH.unshift(lib) if File.directory?(lib) && !$LOAD_PATH.include?(lib)
5
+
6
+ require 'logger'
7
+ require 'trollop'
8
+ require 'terraframe'
9
+ require 'awesome_print'
10
+
11
+ opts = Trollop::options do
12
+ opt :input_file, "Input file(s) to process into a Terraform script.",
13
+ :short => "f", :type => :string, :multi => true, :required => true
14
+ opt :variable_file, "Variable file (YAML or JSON, not tfvars!).",
15
+ :short => "v", :type => :string, :multi => true
16
+ opt :pretty_print, "Pretty-prints the Terraform output.", :default => true
17
+ opt :verbose, "Increases logging verbosity.", :default => false
18
+ end
19
+
20
+ processor = Terraframe::Processor.new
21
+ if opts[:verbose]
22
+ processor.logger.level = Logger::DEBUG
23
+ end
24
+ processor.logger.debug opts.inspect
25
+
26
+ output = processor.process_files(opts[:input_file], opts[:variable_file] || [])
27
+ if opts[:pretty_print]
28
+ output = JSON.pretty_generate(JSON.parse(output))
29
+ end
30
+
31
+ processor.logger.info "Writing output to stdout."
32
+ puts output
@@ -0,0 +1,2 @@
1
+ require "terraframe/version"
2
+ require "terraframe/processor"
@@ -0,0 +1,10 @@
1
+ require 'terraframe/provider'
2
+ require 'terraframe/resource'
3
+
4
+ module Terraframe
5
+ class Context
6
+ def provider_type
7
+ raise "provider_type be implemented in subclassed contexts."
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,20 @@
1
+ require 'terraframe/context'
2
+ require 'terraframe/provider'
3
+
4
+ module Terraframe
5
+ module Contexts
6
+ class AWSContext < Context
7
+ RESOURCES = [
8
+ :aws_instance
9
+ ]
10
+
11
+ def provider_type
12
+ Terraframe::Provider
13
+ end
14
+
15
+ def resources
16
+ RESOURCES
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,64 @@
1
+ require 'logger'
2
+ require 'digest/sha1'
3
+ require 'yaml'
4
+
5
+ require 'active_support/core_ext/hash'
6
+ require 'awesome_print'
7
+
8
+ require 'terraframe/state'
9
+ require 'terraframe/contexts/aws_context'
10
+
11
+ module Terraframe
12
+ class Processor
13
+ attr_reader :logger
14
+ attr_reader :contexts
15
+
16
+
17
+ def initialize
18
+ @logger = Logger.new($stderr)
19
+ logger.level = Logger::INFO
20
+
21
+ logger.debug "Logger initialized."
22
+
23
+ @contexts = {}
24
+ register_context(:aws, Terraframe::Contexts::AWSContext.new)
25
+ end
26
+
27
+ def register_context(name, context)
28
+ name = name.to_sym
29
+ if @contexts[name]
30
+ logger.warn "A context with the name '#{name}' has been registered more than once."
31
+ end
32
+ @contexts[name] = context
33
+ end
34
+
35
+ def process_files(scripts, variables)
36
+ scripts = scripts.map { |f| File.expand_path(f) }
37
+ variables = variables.map { |f| File.expand_path(f) }
38
+
39
+ missing_scripts = scripts.reject { |f| File.exist?(f) }
40
+ missing_variables = variables.reject { |f| File.exist?(f) }
41
+ unless missing_scripts.empty? && missing_variables.empty?
42
+ missing_scripts.each { |f| logger.fatal "Script file not found: #{f}" }
43
+ missing_variables.each { |f| logger.fatal "Variable file not found: #{f}" }
44
+ raise "One or more specified files were missing."
45
+ end
46
+
47
+ apply(Hash[scripts.zip(scripts.map { |f| IO.read(f) })], load_variables(variables))
48
+ end
49
+
50
+ def load_variables(variables)
51
+ vars = {}
52
+ variables.each { |f| vars = vars.deep_merge(YAML::load_file(f)) }
53
+ vars
54
+ end
55
+
56
+ def apply(inputs, vars)
57
+ logger.info "Beginning state execution."
58
+
59
+ state = State.new(logger, vars, @contexts)
60
+ inputs.each { |input| state.__apply_script(input[0], input[1])}
61
+ state.__build()
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,6 @@
1
+ require 'terraframe/script_item'
2
+
3
+ module Terraframe
4
+ class Provider < Terraframe::ScriptItem
5
+ end
6
+ end
@@ -0,0 +1,9 @@
1
+ require 'terraframe/script_item'
2
+
3
+ module Terraframe
4
+ class Resource < Terraframe::ScriptItem
5
+ def resource_type
6
+ raise "resource_type must be overridden in inheriting classes."
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,36 @@
1
+ require 'json'
2
+
3
+ module Terraframe
4
+ class ScriptItem
5
+ attr_reader :fields
6
+ attr_reader :vars
7
+
8
+ def initialize(vars, &block)
9
+ @fields = {}
10
+ @vars = vars
11
+
12
+ instance_eval &block
13
+ end
14
+
15
+ def to_json(*a)
16
+ @fields.to_json(*a)
17
+ end
18
+
19
+ ## DSL FUNCTIONS BELOW
20
+ def method_missing(method_name, *args, &block)
21
+ if args.length == 1
22
+ @fields[method_name.to_sym] = args[0]
23
+ else
24
+ raise "Multiple fields passed to a scalar auto-argument '#{method_name}'."
25
+ end
26
+ end
27
+
28
+
29
+ def output_of(resource_type, resource_name, output_type)
30
+ "${#{resource_type}.#{resource_name}.#{output_type}}"
31
+ end
32
+ def id_of(resource_type, resource_name)
33
+ output_of(resource_type, resource_name, :id)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,96 @@
1
+ require 'json'
2
+ require 'recursive_open_struct'
3
+
4
+ module Terraframe
5
+ class State
6
+ attr_reader :vars
7
+ attr_reader :logger
8
+
9
+ def initialize(logger, vars, contexts)
10
+ @logger = logger
11
+ logger.info "Initializing state."
12
+
13
+ @vars = RecursiveOpenStruct.new(vars, :recurse_over_arrays => true )
14
+ logger.debug "State variables:"
15
+ logger.ap vars, :debug
16
+
17
+ @__contexts = contexts
18
+
19
+ @__output = {
20
+ :provider => {},
21
+ :variable => {},
22
+ :resource => {}
23
+ }
24
+ end
25
+
26
+ def __build()
27
+ logger.info "Building Terraform script from state."
28
+ logger.debug "Contexts:"
29
+ @__contexts.each { |c| logger.debug " - #{c}" }
30
+ @__output.to_json
31
+ end
32
+
33
+ def __apply_script(script_name, script)
34
+ logger.info "Applying script '#{script_name}' to state."
35
+ instance_eval(script, script_name, 0)
36
+ logger.info "Script '#{script_name}' applied successfully."
37
+ end
38
+
39
+ ## DSL FUNCTIONS BELOW ##
40
+ def provider(type, &block)
41
+ if !@__contexts[type]
42
+ msg = "Unknown provider type: '#{type}'."
43
+ logger.fatal msg
44
+ raise msg
45
+ end
46
+
47
+ if @__output[:provider][type]
48
+ msg = "Duplicate provider type (sorry, blame Terraform): '#{type}'"
49
+ logger.fatal msg
50
+ raise msg
51
+ end
52
+
53
+ provider = @__contexts[type].provider_type.new(vars, &block)
54
+ logger.debug "Provider of type '#{type}': #{provider.inspect}"
55
+ @__output[:provider][type] = provider
56
+
57
+ provider
58
+ end
59
+
60
+ def variable
61
+ msg = "TODO: implement tfvar support."
62
+ logger.fatal msg
63
+ raise msg
64
+ end
65
+
66
+ def provisioner
67
+ msg = "TODO: implement provisioner support."
68
+ logger.fatal msg
69
+ raise msg
70
+ end
71
+
72
+ def resource(resource_type, resource_name, &block)
73
+ unless @__contexts.any? { |k, v| v.resources.include?(resource_type) }
74
+ logger.warn "Could not find a context that supports resource type '#{resource_type}'. Continuing, but you've been warned."
75
+ end
76
+
77
+ @__output[:resource][resource_type] ||= {}
78
+ @__output[:resource][resource_type][resource_name.to_s] = Resource.new(vars, &block)
79
+ end
80
+
81
+ # anything that is not a provider or a variable should be interpreted
82
+ def method_missing(method_name, *args, &block)
83
+ case method_name
84
+ when "vars"
85
+ @vars
86
+ else
87
+ if (args.length != 1)
88
+ msg = "Too many arguments for resource invocation '#{method_name}'."
89
+ logger.fatal(msg)
90
+ raise msg
91
+ end
92
+ resource(method_name.to_sym, args[0], &block)
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,3 @@
1
+ module Terraframe
2
+ VERSION = "0.0.5"
3
+ end
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'terraframe/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "terraframe"
8
+ spec.version = Terraframe::VERSION
9
+ spec.authors = ["Ed Ropple"]
10
+ spec.email = ["ed@edropple.com"]
11
+ spec.summary = "A sane Ruby-based DSL for emitting Terraform scripts."
12
+ # spec.description = %q{TODO: Write a longer description. Optional.}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.7"
22
+ spec.add_development_dependency "rake", "~> 10.0"
23
+
24
+ spec.add_runtime_dependency "trollop", "~> 2.0"
25
+ spec.add_runtime_dependency "activesupport", "~> 4.2.0"
26
+ spec.add_runtime_dependency "awesome_print", "~> 1.6.1"
27
+ spec.add_runtime_dependency "recursive-open-struct", "~> 0.5.0"
28
+ end
File without changes
File without changes
@@ -0,0 +1,12 @@
1
+ provider :aws do
2
+ access_key "boop"
3
+ secret_key "bop"
4
+
5
+ region "us-east-1"
6
+ end
7
+
8
+ (0...vars.test_vars.num_jimmies).each do |i|
9
+ aws_instance "jimmy-#{i}" do
10
+ vpc_id id_of(:aws_vpc, "main-vpc")
11
+ end
12
+ end
@@ -0,0 +1,2 @@
1
+ aws:
2
+ thing: 5
File without changes
@@ -0,0 +1,2 @@
1
+ test_vars:
2
+ num_jimmies: 6
metadata ADDED
@@ -0,0 +1,157 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: terraframe
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.5
5
+ platform: ruby
6
+ authors:
7
+ - Ed Ropple
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-01-10 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: '1.7'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.7'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: trollop
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: activesupport
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 4.2.0
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 4.2.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: awesome_print
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 1.6.1
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 1.6.1
83
+ - !ruby/object:Gem::Dependency
84
+ name: recursive-open-struct
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 0.5.0
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 0.5.0
97
+ description:
98
+ email:
99
+ - ed@edropple.com
100
+ executables:
101
+ - terraframe
102
+ extensions: []
103
+ extra_rdoc_files: []
104
+ files:
105
+ - ".gitignore"
106
+ - Gemfile
107
+ - LICENSE.txt
108
+ - README.md
109
+ - Rakefile
110
+ - bin/terraframe
111
+ - lib/terraframe.rb
112
+ - lib/terraframe/context.rb
113
+ - lib/terraframe/contexts/aws_context.rb
114
+ - lib/terraframe/processor.rb
115
+ - lib/terraframe/provider.rb
116
+ - lib/terraframe/resource.rb
117
+ - lib/terraframe/script_item.rb
118
+ - lib/terraframe/state.rb
119
+ - lib/terraframe/version.rb
120
+ - terraframe.gemspec
121
+ - test/aws_tests.rb
122
+ - test/basic_tests.rb
123
+ - test/scripts/aws.terraframe
124
+ - test/scripts/aws_vars.yaml
125
+ - test/scripts/basic.terraframe
126
+ - test/scripts/basic_vars.yaml
127
+ homepage: ''
128
+ licenses:
129
+ - MIT
130
+ metadata: {}
131
+ post_install_message:
132
+ rdoc_options: []
133
+ require_paths:
134
+ - lib
135
+ required_ruby_version: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - ">="
138
+ - !ruby/object:Gem::Version
139
+ version: '0'
140
+ required_rubygems_version: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ requirements: []
146
+ rubyforge_project:
147
+ rubygems_version: 2.4.5
148
+ signing_key:
149
+ specification_version: 4
150
+ summary: A sane Ruby-based DSL for emitting Terraform scripts.
151
+ test_files:
152
+ - test/aws_tests.rb
153
+ - test/basic_tests.rb
154
+ - test/scripts/aws.terraframe
155
+ - test/scripts/aws_vars.yaml
156
+ - test/scripts/basic.terraframe
157
+ - test/scripts/basic_vars.yaml