terraframe 0.0.5

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
+ 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