terraform_dsl 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/tasks/tasks.rb +12 -0
- data/lib/tasks/terraform.rake +126 -0
- data/lib/terraform_dsl.rb +4 -0
- data/lib/terraform_dsl/aws/remote_s3.rb +20 -0
- data/lib/terraform_dsl/aws/tagging.rb +23 -0
- data/lib/terraform_dsl/command.rb +58 -0
- data/lib/terraform_dsl/module.rb +36 -0
- data/lib/terraform_dsl/output.rb +6 -0
- data/lib/terraform_dsl/provider.rb +13 -0
- data/lib/terraform_dsl/remote.rb +29 -0
- data/lib/terraform_dsl/resource.rb +44 -0
- data/lib/terraform_dsl/secrets.rb +29 -0
- data/lib/terraform_dsl/stack.rb +79 -0
- data/lib/terraform_dsl/stack_element.rb +62 -0
- data/lib/terraform_dsl/stack_module.rb +123 -0
- data/lib/terraform_dsl/stack_modules.rb +24 -0
- data/lib/terraform_dsl/stacks.rb +64 -0
- data/lib/terraform_dsl/variable.rb +6 -0
- metadata +91 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: d5ba050edd5f77df1afad2a171fe81b140f1ad09
|
4
|
+
data.tar.gz: eb7d97ed6e27a68abac7a3c63607f67c6c12da0f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2b9e2eda30ddc8943012088c07bb53718daba7ac23a3aaebe3233c528ee39e732dc0fa8ca681d5b3386209a3d3c53e70605cad60eb53c267963dfe688274830f
|
7
|
+
data.tar.gz: 8b1eda39d07c1921b212ea16e197d42ef2c8d9e87451b0ebb4f1ada4dc38c252d936862769ce3f18f2603c6c52e7b79b193440caaf8b05cb037b10f4d46aa110
|
data/lib/tasks/tasks.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'rake'
|
2
|
+
|
3
|
+
module Terraform
|
4
|
+
# Loads all rake tasks when terraform_dsl is included by a rake script
|
5
|
+
class Tasks
|
6
|
+
def self.loadall
|
7
|
+
Dir.glob("#{File.join(File.dirname(__dir__), 'tasks')}/*.rake").each { |r| load r }
|
8
|
+
template_path = ENV['TERRAFORM_TEMPLATE_PATH'] || 'templates'
|
9
|
+
Dir.glob("#{File.join(Dir.pwd,template_path)}/*.rb").each { |t| require_relative t }
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
require_relative '../terraform_dsl.rb'
|
2
|
+
namespace 'terraform' do
|
3
|
+
desc 'Loads available stack modules'
|
4
|
+
task :load do
|
5
|
+
Terraform::Stacks.load
|
6
|
+
end
|
7
|
+
|
8
|
+
desc 'Initialize stacks.yml if it does not exist'
|
9
|
+
task :init do
|
10
|
+
Terraform::Stacks.init
|
11
|
+
end
|
12
|
+
|
13
|
+
desc 'Fetch modules for a stack'
|
14
|
+
task :get, [:name] => :load do |_t, args|
|
15
|
+
stack = Terraform::Stacks.stacks[args[:name]]
|
16
|
+
stack.get
|
17
|
+
end
|
18
|
+
|
19
|
+
desc 'Fetch modules for all stacks'
|
20
|
+
task get_all: :load do
|
21
|
+
Terraform::Stacks.stacks.each do |_name, stack|
|
22
|
+
stack.get
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
desc 'List all available stacks'
|
27
|
+
task list: :load do
|
28
|
+
Terraform::Stacks.stacks.each do |name, _stack|
|
29
|
+
puts name
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
desc 'Show details about a stack by name'
|
34
|
+
task :show, [:name] => :load do |_t, args|
|
35
|
+
fail 'Specify a name' unless args[:name]
|
36
|
+
stack = Terraform::Stacks.stacks[args[:name]]
|
37
|
+
puts stack
|
38
|
+
stack.plan
|
39
|
+
end
|
40
|
+
|
41
|
+
desc 'Show all pending stack changes'
|
42
|
+
task show_all: [:load, :get_all] do
|
43
|
+
Terraform::Stacks.stacks.each do |_name, stack|
|
44
|
+
puts stack
|
45
|
+
stack.plan
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
desc 'Show pending changes for a stack'
|
50
|
+
task :plan, [:name] => :load do |_t, args|
|
51
|
+
fail 'Specify a name' unless args[:name]
|
52
|
+
stack = Terraform::Stacks.stacks[args[:name]]
|
53
|
+
stack.plan
|
54
|
+
end
|
55
|
+
|
56
|
+
desc 'Apply pending changes for a stack'
|
57
|
+
task :apply, [:name] => :load do |_t, args|
|
58
|
+
fail 'Specify a name' unless args[:name]
|
59
|
+
stack = Terraform::Stacks.stacks[args[:name]]
|
60
|
+
stack.apply
|
61
|
+
end
|
62
|
+
|
63
|
+
desc 'Apply all pending stack changes'
|
64
|
+
task apply_all: :load do
|
65
|
+
Terraform::Stacks.stacks.each do |_name, stack|
|
66
|
+
puts stack
|
67
|
+
stack.apply
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
desc 'Destroy a stack'
|
72
|
+
task :destroy, [:name] => :load do |_t, args|
|
73
|
+
fail 'Specify a name' unless args[:name]
|
74
|
+
stack = Terraform::Stacks.stacks[args[:name]]
|
75
|
+
stack.destroy
|
76
|
+
end
|
77
|
+
|
78
|
+
desc 'Push stack state to remote location'
|
79
|
+
task :push, [:name] => [:load, :remote_config] do |_t, args|
|
80
|
+
stack = Terraform::Stacks.stacks[args[:name]]
|
81
|
+
stack.push
|
82
|
+
end
|
83
|
+
|
84
|
+
desc 'Push stack states to remote location'
|
85
|
+
task push_all: [:load, :remote_config_all] do
|
86
|
+
Terraform::Stacks.stacks.each do |_name, stack|
|
87
|
+
puts stack
|
88
|
+
stack.push
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
desc 'Pulls stack state from remote location'
|
93
|
+
task :pull, [:name] => [:load, :remote_config] do |_t, args|
|
94
|
+
stack = Terraform::Stacks.stacks[args[:name]]
|
95
|
+
stack.pull
|
96
|
+
end
|
97
|
+
|
98
|
+
desc 'Pulls stack states from remote location'
|
99
|
+
task pull_all: [:load, :remote_config_all] do
|
100
|
+
Terraform::Stacks.stacks.each do |_name, stack|
|
101
|
+
puts stack
|
102
|
+
stack.pull
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
desc 'Sets up remote config for a stack'
|
107
|
+
task :remote_config, [:name] => [:load] do |_t, args|
|
108
|
+
stack = Terraform::Stacks.stacks[args[:name]]
|
109
|
+
stack.remote_config
|
110
|
+
end
|
111
|
+
|
112
|
+
desc 'Sets up remote config for all stacks that support it'
|
113
|
+
task remote_config_all: :load do
|
114
|
+
Terraform::Stacks.stacks.each do |_name, stack|
|
115
|
+
puts stack
|
116
|
+
stack.remote_config
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
desc 'Generate a UUID for a stack, so stacks do not clobber each other'
|
121
|
+
task :uuid do
|
122
|
+
uuid = Terraform::Stacks.uuid
|
123
|
+
puts "uuid: #{uuid}"
|
124
|
+
puts 'Add this as a field to a new stack to prevent clobbering stacks with the same name'
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Terraform
|
2
|
+
module Aws
|
3
|
+
# Support AWS S3 remote state backend
|
4
|
+
module Remote_S3
|
5
|
+
private
|
6
|
+
|
7
|
+
def options_for_s3(command)
|
8
|
+
options = ''
|
9
|
+
options = "-backend=s3 #{config_opts_s3}" if command == :config
|
10
|
+
"terraform remote #{command} #{options}"
|
11
|
+
end
|
12
|
+
|
13
|
+
def config_opts_s3
|
14
|
+
config = "-backend-config='key=#{@stack.stack_id}' "
|
15
|
+
config += @stack.remote['s3'].map { |k, v| "-backend-config='#{k}=#{v}'" }.join(' ')
|
16
|
+
config
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Terraform
|
2
|
+
# Aws provider-specific functionality, to be mixed in to stack elements
|
3
|
+
module Aws
|
4
|
+
def self.taggable?(resource_type)
|
5
|
+
supports_tagging = [:aws_autoscaling_group, :aws_customer_gateway,
|
6
|
+
:aws_db_instance, :aws_elasticache_cluster,
|
7
|
+
:aws_elb, :aws_instance, :aws_internet_gateway,
|
8
|
+
:aws_network_acl, :aws_network_interface, :aws_route53_zone,
|
9
|
+
:aws_route_table, :aws_s3_bucket, :aws_security_group,
|
10
|
+
:aws_subnet, :aws_vpc, :aws_vpc_dhcp_options,
|
11
|
+
:aws_vpc_peering_connection, :aws_vpn_connection,
|
12
|
+
:aws_vpn_gateway]
|
13
|
+
supports_tagging.include?(resource_type.to_sym)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Tag all resources (that support tagging) that we created with this stack id
|
17
|
+
def post_processing_aws
|
18
|
+
return unless Aws.taggable?(@resource_type)
|
19
|
+
@fields[:tags] ||= {}
|
20
|
+
@fields[:tags][:terraform_stack_id] = var(:terraform_stack_id)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Terraform
|
2
|
+
# Wraps terraform command execution
|
3
|
+
class Command
|
4
|
+
attr_accessor :command
|
5
|
+
|
6
|
+
def initialize(stack, command)
|
7
|
+
@stack = stack
|
8
|
+
@command = options_for(command)
|
9
|
+
prepare
|
10
|
+
end
|
11
|
+
|
12
|
+
def env
|
13
|
+
vars = {}
|
14
|
+
@stack.variables.each { |k, v| vars["TF_VAR_#{k}"] = v[:default] }
|
15
|
+
@stack.module.secrets.each do |_provider, secrets|
|
16
|
+
secrets.each do |k, v|
|
17
|
+
vars[k.to_s] = v
|
18
|
+
end
|
19
|
+
end
|
20
|
+
vars
|
21
|
+
end
|
22
|
+
|
23
|
+
def execute
|
24
|
+
Process.waitpid(spawn(env, @command, chdir: @stack.stack_dir))
|
25
|
+
fail 'Command failed' unless $CHILD_STATUS.to_i == 0
|
26
|
+
end
|
27
|
+
|
28
|
+
protected
|
29
|
+
|
30
|
+
def prepare
|
31
|
+
FileUtils.mkdir_p(@stack.stack_dir)
|
32
|
+
File.open(File.join(@stack.stack_dir, 'terraform.tf.json'), 'w') { |f| f.write(generate) }
|
33
|
+
end
|
34
|
+
|
35
|
+
def options_for(cmd)
|
36
|
+
options = begin
|
37
|
+
case cmd
|
38
|
+
when :apply
|
39
|
+
'-input=false'
|
40
|
+
when :destroy
|
41
|
+
'-input=false -force'
|
42
|
+
when :plan
|
43
|
+
'-input=false -module-depth=-1'
|
44
|
+
when :graph
|
45
|
+
'-draw-cycles'
|
46
|
+
else
|
47
|
+
''
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
"terraform #{cmd} #{options}"
|
52
|
+
end
|
53
|
+
|
54
|
+
def generate
|
55
|
+
@stack.module.generate
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require_relative 'stack_element'
|
2
|
+
require_relative 'stack_modules'
|
3
|
+
require_relative 'stack_module'
|
4
|
+
require_relative 'stacks'
|
5
|
+
|
6
|
+
module Terraform
|
7
|
+
class Module < StackElement
|
8
|
+
def initialize(parent_module, module_name, &block)
|
9
|
+
super(parent_module, &block)
|
10
|
+
|
11
|
+
if @fields[:source].match(/^module_/)
|
12
|
+
build_submodule(parent_module, module_name)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def build_submodule(parent_module, module_name)
|
19
|
+
generated = generate_child_module(parent_module)
|
20
|
+
module_path = File.join(Stacks.dir, parent_module.id, module_name.to_s)
|
21
|
+
FileUtils.mkdir_p(module_path)
|
22
|
+
File.open(File.join(module_path, 'stack_module.tf.json'),'w') { |f| f.write(generated) }
|
23
|
+
@fields[:source] = File.expand_path(module_path)
|
24
|
+
end
|
25
|
+
|
26
|
+
def generate_child_module(parent_module)
|
27
|
+
variables = @fields.clone
|
28
|
+
variables.delete(:source)
|
29
|
+
variables[:terraform_stack_id] = parent_module.id
|
30
|
+
child_name = @fields[:source].gsub(/^module_/,'')
|
31
|
+
child_module = StackModules.get(child_name)
|
32
|
+
child_module.build(variables)
|
33
|
+
child_module.generate
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require_relative 'secrets.rb'
|
2
|
+
require_relative 'stack_element.rb'
|
3
|
+
|
4
|
+
module Terraform
|
5
|
+
# Implements DSL for a terraform provider, and a means of loading secrets.
|
6
|
+
class Provider < StackElement
|
7
|
+
def load_secrets(name)
|
8
|
+
@secrets ||= {}
|
9
|
+
@secrets[name.to_sym] = SECRETS[name.to_sym]
|
10
|
+
@secrets
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require_relative 'aws/remote_s3.rb'
|
2
|
+
|
3
|
+
module Terraform
|
4
|
+
class Remote < Command
|
5
|
+
class RemoteNotSupported < Exception; end
|
6
|
+
include Aws::Remote_S3
|
7
|
+
def initialize(stack, command)
|
8
|
+
super
|
9
|
+
unless @stack.remote.first
|
10
|
+
puts "\tWARNING: #{@stack.name} is not configured with a remote backend"
|
11
|
+
return
|
12
|
+
end
|
13
|
+
|
14
|
+
backend = @stack.remote.keys.first
|
15
|
+
sym = "options_for_#{backend}"
|
16
|
+
|
17
|
+
if self.respond_to?(sym, include_private: true)
|
18
|
+
@command = send(sym, command)
|
19
|
+
else
|
20
|
+
fail RemoteNotSupported, "Remote backend #{backend} is not supported yet"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def execute
|
25
|
+
return unless @stack.remote.first
|
26
|
+
super
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require_relative 'stack_element.rb'
|
2
|
+
|
3
|
+
module Terraform
|
4
|
+
# Defines a terraform resource
|
5
|
+
class Resource < StackElement
|
6
|
+
attr_reader :resource_name
|
7
|
+
|
8
|
+
def initialize(parent_module, resource_name, resource_type, &block)
|
9
|
+
@resource_name = resource_name
|
10
|
+
@resource_type = resource_type
|
11
|
+
super(parent_module, &block)
|
12
|
+
|
13
|
+
# Allow provider specific post processing
|
14
|
+
sym = "post_processing_#{resource_type.split('_').first}"
|
15
|
+
send(sym) if self.respond_to?(sym, include_private: true)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Allow provisioner blocks to be nested within resources
|
19
|
+
def provisioner(provisioner_type, &block)
|
20
|
+
provisioner_type = provisioner_type.to_sym
|
21
|
+
|
22
|
+
@fields[:provisioner] = @fields[:provisioner] || []
|
23
|
+
|
24
|
+
provisioner_set = Provisioner.new(@module, &block)
|
25
|
+
@fields[:provisioner] << { cleanup_provisioner_type(provisioner_type) => provisioner_set.fields }
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def cleanup_provisioner_type(provisioner_type)
|
31
|
+
case provisioner_type.to_sym
|
32
|
+
when :remote_exec
|
33
|
+
'remote-exec'
|
34
|
+
when :local_exec
|
35
|
+
'local-exec'
|
36
|
+
else
|
37
|
+
provisioner_type
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
# Defines a terraform resource provisioner
|
42
|
+
class Provisioner < StackElement
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'open3'
|
3
|
+
|
4
|
+
# Load and provide access to secrets required by terraform providers
|
5
|
+
class SecretHash < Hash
|
6
|
+
class SecretNotFound < Exception; end
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
super { |_secrets, key| fail SecretNotFound, "Secret `#{key}` not found" }
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
if ENV['TERRAFORM_ENV'] == 'test'
|
14
|
+
SECRETS ||= {
|
15
|
+
aws: {
|
16
|
+
access_key_id: 'foo',
|
17
|
+
secret_access_key: 'bar'
|
18
|
+
}
|
19
|
+
}
|
20
|
+
else
|
21
|
+
SECRETS ||= begin
|
22
|
+
secrets_file = File.expand_path(ENV['CONFIG_PATH'] || 'config/secrets.json')
|
23
|
+
if File.exist?(secrets_file)
|
24
|
+
JSON.parse(File.read(secrets_file), symbolize_names: true, object_class: SecretHash)
|
25
|
+
else
|
26
|
+
{}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require_relative 'stacks'
|
2
|
+
require_relative 'stack_modules'
|
3
|
+
require_relative 'command'
|
4
|
+
require_relative 'remote'
|
5
|
+
|
6
|
+
module Terraform
|
7
|
+
# Wrapper to instantiate a stack from a yaml definition
|
8
|
+
class Stack
|
9
|
+
class MalformedConfig < Exception; end
|
10
|
+
class << self
|
11
|
+
def load(config)
|
12
|
+
fail MalformedConfig, "Configuration malformed at #{config}" unless config.is_a?(Hash)
|
13
|
+
fail MalformedConfig, "A name must be specified for the stack #{config}" unless config.key?('name')
|
14
|
+
fail MalformedConfig, 'You must specify a uuid. Get one from rake uuid and add it to the config' unless config.key?('uuid')
|
15
|
+
new(config)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_reader :name, :description, :module,
|
20
|
+
:stack_dir, :stack_id, :remote
|
21
|
+
|
22
|
+
def initialize(config)
|
23
|
+
@name = config['name']
|
24
|
+
@uuid = config['uuid']
|
25
|
+
@description = config['description'] || ''
|
26
|
+
@variables = config['variables'] || {}
|
27
|
+
@remote = config['remote'] || {}
|
28
|
+
@stack_id = "terraform_#{@name}_#{@uuid}"
|
29
|
+
@module = StackModules.get(config['root'])
|
30
|
+
@variables['terraform_stack_id'] = @stack_id
|
31
|
+
@stack_dir = File.join(Stacks.dir, @stack_id)
|
32
|
+
@module.build(@variables.map { |k, v| [k.to_sym, v] }.to_h)
|
33
|
+
end
|
34
|
+
|
35
|
+
def apply
|
36
|
+
Command.new(self, :apply).execute
|
37
|
+
end
|
38
|
+
|
39
|
+
def destroy
|
40
|
+
Command.new(self, :destroy).execute
|
41
|
+
end
|
42
|
+
|
43
|
+
def plan
|
44
|
+
Command.new(self, :plan).execute
|
45
|
+
end
|
46
|
+
|
47
|
+
def get
|
48
|
+
Command.new(self, :get).execute
|
49
|
+
end
|
50
|
+
|
51
|
+
def show
|
52
|
+
Command.new(self, :show).execute
|
53
|
+
end
|
54
|
+
|
55
|
+
def pull
|
56
|
+
Remote.new(self, :pull).execute
|
57
|
+
end
|
58
|
+
|
59
|
+
def push
|
60
|
+
Remote.new(self, :pull).execute
|
61
|
+
end
|
62
|
+
|
63
|
+
def remote_config
|
64
|
+
Remote.new(self, :config).execute
|
65
|
+
end
|
66
|
+
|
67
|
+
def variables
|
68
|
+
@module.variables
|
69
|
+
end
|
70
|
+
|
71
|
+
def to_s
|
72
|
+
<<-eos
|
73
|
+
Name: #{@name}
|
74
|
+
Description: #{@description}
|
75
|
+
Stack Directory: #{@stack_dir}
|
76
|
+
eos
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require_relative 'aws/tagging.rb'
|
2
|
+
|
3
|
+
module Terraform
|
4
|
+
# Defines a single terraform stack element, subclass for any element defined in terraform DSL
|
5
|
+
class StackElement
|
6
|
+
include Aws
|
7
|
+
attr_reader :fields
|
8
|
+
|
9
|
+
def initialize(stack_module, &block)
|
10
|
+
@module = stack_module
|
11
|
+
@fields = {}
|
12
|
+
instance_eval(&block)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
# Allows resource attributes to be specified with a nice syntax
|
18
|
+
# If the method is implemented, it will be treated as a nested resource
|
19
|
+
def method_missing(method_name, *args, &block)
|
20
|
+
symbol = method_name.to_sym
|
21
|
+
if args.length == 1
|
22
|
+
if args[0].nil?
|
23
|
+
fail "Passed nil to '#{method_name}'. Generally disallowed, subclass StackElement if you need this."
|
24
|
+
end
|
25
|
+
add_field(symbol, args[0])
|
26
|
+
else
|
27
|
+
add_field(symbol, Terraform::StackElement.new(@module, &block).fields)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Get the runtime value of a variable
|
32
|
+
def get(variable_name)
|
33
|
+
@module.get(variable_name)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Reference a variable
|
37
|
+
def var(variable_name)
|
38
|
+
"${var.#{variable_name}}"
|
39
|
+
end
|
40
|
+
|
41
|
+
# Syntax to handle interpolation of resource variables
|
42
|
+
def value_of(resource_type, resource_name, value_type)
|
43
|
+
"${#{resource_type}.#{resource_name}.#{value_type}}"
|
44
|
+
end
|
45
|
+
|
46
|
+
# Shorthand to interpolate the ID of another resource
|
47
|
+
def id_of(resource_type, resource_name)
|
48
|
+
value_of(resource_type, resource_name, :id)
|
49
|
+
end
|
50
|
+
|
51
|
+
def add_field(symbol, value)
|
52
|
+
existing = @fields[symbol]
|
53
|
+
if existing
|
54
|
+
# If it's already an array, just push to it
|
55
|
+
@fields[symbol] = [existing] unless existing.is_a?(Array)
|
56
|
+
@fields[symbol] << value
|
57
|
+
else
|
58
|
+
@fields[symbol] = value
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
require_relative 'stack_modules.rb'
|
5
|
+
require_relative 'resource.rb'
|
6
|
+
require_relative 'provider.rb'
|
7
|
+
require_relative 'variable.rb'
|
8
|
+
require_relative 'module.rb'
|
9
|
+
require_relative 'output.rb'
|
10
|
+
|
11
|
+
module Terraform
|
12
|
+
|
13
|
+
class StackModule
|
14
|
+
|
15
|
+
class << self
|
16
|
+
def define(name, &block)
|
17
|
+
template = self.new(name, &block)
|
18
|
+
StackModules.register(name, template)
|
19
|
+
end
|
20
|
+
|
21
|
+
def flatten_variable_arrays(variables)
|
22
|
+
variables.map do |k,v|
|
23
|
+
if v.is_a?(Hash) && v.key?(:default) && v[:default].is_a?(Array)
|
24
|
+
v[:default] = v[:default].join(',')
|
25
|
+
elsif v.is_a?(Array)
|
26
|
+
v = v.join(',')
|
27
|
+
end
|
28
|
+
[k, v]
|
29
|
+
end.to_h
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
attr_accessor :secrets
|
35
|
+
|
36
|
+
def initialize(name, &block)
|
37
|
+
@stack_elements = { resource: {}, provider: {}, variable: {}, output: {}, module: {} }
|
38
|
+
@secrets = {}
|
39
|
+
@block = block
|
40
|
+
variable(:terraform_stack_id) {}
|
41
|
+
end
|
42
|
+
|
43
|
+
def clone
|
44
|
+
b = @block
|
45
|
+
StackModule.new(@name, &b)
|
46
|
+
end
|
47
|
+
|
48
|
+
def build(**kwargs)
|
49
|
+
vars = kwargs.map{ |k, v| [k, {default: v}] }.to_h
|
50
|
+
@stack_elements[:variable].merge!(vars)
|
51
|
+
b = @block
|
52
|
+
instance_eval(&b)
|
53
|
+
end
|
54
|
+
|
55
|
+
def generate
|
56
|
+
JSON.pretty_generate(elements)
|
57
|
+
end
|
58
|
+
|
59
|
+
def variables
|
60
|
+
elements[:variable]
|
61
|
+
end
|
62
|
+
|
63
|
+
def outputs
|
64
|
+
@stack_elements[:output]
|
65
|
+
end
|
66
|
+
|
67
|
+
def get(variable)
|
68
|
+
@stack_elements[:variable].fetch(variable)[:default]
|
69
|
+
end
|
70
|
+
|
71
|
+
def elements
|
72
|
+
elements = @stack_elements.clone
|
73
|
+
variables = StackModule.flatten_variable_arrays(@stack_elements[:variable])
|
74
|
+
@stack_elements[:module].each do |mod, data|
|
75
|
+
elements[:module][mod] = StackModule.flatten_variable_arrays(data)
|
76
|
+
end
|
77
|
+
elements[:variable] = variables
|
78
|
+
elements
|
79
|
+
end
|
80
|
+
|
81
|
+
def id
|
82
|
+
get(:terraform_stack_id)
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def register_resource(resource_type, name, &block)
|
88
|
+
@stack_elements[:resource] ||= {}
|
89
|
+
@stack_elements[:resource][resource_type.to_sym] ||= {}
|
90
|
+
@stack_elements[:resource][resource_type.to_sym][name.to_sym] = Terraform::Resource.new(self, name, resource_type, &block).fields
|
91
|
+
end
|
92
|
+
|
93
|
+
def register_variable(name, &block)
|
94
|
+
new_variable = Terraform::Variable.new(self, &block).fields
|
95
|
+
unless @stack_elements[:variable].key?(name.to_sym)
|
96
|
+
@stack_elements[:variable][name.to_sym] = { default: new_variable[:default] || '' }
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def register_output(name, &block)
|
101
|
+
new_output = Terraform::Output.new(self, &block).fields
|
102
|
+
@stack_elements[:output][name.to_sym] = new_output
|
103
|
+
end
|
104
|
+
|
105
|
+
def register_module(name, &block)
|
106
|
+
new_module = Terraform::Module.new(self, name, &block).fields
|
107
|
+
@stack_elements[:module][name.to_sym] = new_module
|
108
|
+
end
|
109
|
+
|
110
|
+
def register_provider(name, &block)
|
111
|
+
provider = Terraform::Provider.new(self, &block)
|
112
|
+
@secrets.merge!(provider.load_secrets(name))
|
113
|
+
@stack_elements[:provider][name.to_sym] = provider.fields
|
114
|
+
end
|
115
|
+
|
116
|
+
alias_method :resource, :register_resource
|
117
|
+
alias_method :variable, :register_variable
|
118
|
+
alias_method :provider, :register_provider
|
119
|
+
alias_method :output, :register_output
|
120
|
+
alias_method :submodule, :register_module
|
121
|
+
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Terraform
|
2
|
+
# Stack module factory, register a module and provide clones of it
|
3
|
+
class StackModules
|
4
|
+
class ModuleNotFound < StandardError; end
|
5
|
+
class ModuleAlreadyRegistered < StandardError; end
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def register(name, stack_module)
|
9
|
+
fail ModuleAlreadyRegistered, "#{name} is already a registered stack_module" if @stack_modules.key?(name.downcase)
|
10
|
+
@stack_modules[name.downcase] = stack_module
|
11
|
+
end
|
12
|
+
|
13
|
+
def get(stack_module_name)
|
14
|
+
fail ModuleNotFound, "#{stack_module_name} module module not found" unless @stack_modules.key?(stack_module_name.downcase)
|
15
|
+
@stack_modules[stack_module_name.downcase].clone
|
16
|
+
end
|
17
|
+
|
18
|
+
def reset!
|
19
|
+
@stack_modules ||= {}
|
20
|
+
end
|
21
|
+
end
|
22
|
+
reset!
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'securerandom'
|
3
|
+
|
4
|
+
require_relative 'stack.rb'
|
5
|
+
|
6
|
+
module Terraform
|
7
|
+
# Singleton to keep track of stack templates
|
8
|
+
class Stacks
|
9
|
+
class MalformedConfig < StandardError; end
|
10
|
+
class << self
|
11
|
+
attr_reader :stacks
|
12
|
+
|
13
|
+
def load
|
14
|
+
config = ENV['STACK_CONFIG'] || 'stacks.yml'
|
15
|
+
fail 'stacks.yml must exist in root directory, or specify STACK_CONFIG pointing to stacks.yml' unless File.exist?(config)
|
16
|
+
stack_specs = YAML.load(File.read(config))
|
17
|
+
fail MalformedConfig, 'Stacks must be an array' unless stack_specs.key?('stacks') && stack_specs['stacks'].is_a?(Array)
|
18
|
+
common = stack_specs['common'] || {}
|
19
|
+
stack_specs['stacks'].each do |stack_spec|
|
20
|
+
stack = Stack.load(stack_spec.merge(common))
|
21
|
+
@stacks[stack.name] = stack
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def init
|
26
|
+
config = ENV['STACK_CONFIG'] || 'stacks.yml'
|
27
|
+
fail "stacks.yaml already exists at #{File.expand_path(config)}" if File.exist?(config)
|
28
|
+
File.open(config,'w') do |f|
|
29
|
+
f.write(YAML.dump(base_config))
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def uuid
|
34
|
+
"#{Time.now.utc.to_i}_#{SecureRandom.urlsafe_base64(6)}"
|
35
|
+
end
|
36
|
+
|
37
|
+
def base_config
|
38
|
+
{
|
39
|
+
'common' => {},
|
40
|
+
'stacks' => [ base_stack_config ],
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
def base_stack_config
|
45
|
+
{
|
46
|
+
'name' => 'SET_NAME',
|
47
|
+
'uuid' => uuid,
|
48
|
+
'description' => 'SET_A_DESCRIPTION',
|
49
|
+
'root' => 'SET_A_TEMPLATE',
|
50
|
+
'variables' => {},
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
def reset!
|
55
|
+
@stacks ||= {}
|
56
|
+
end
|
57
|
+
|
58
|
+
def dir
|
59
|
+
File.expand_path(File.join(ENV['TERRAFORM_DATA_DIR'] || 'data', 'stacks'))
|
60
|
+
end
|
61
|
+
end
|
62
|
+
reset!
|
63
|
+
end
|
64
|
+
end
|
metadata
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: terraform_dsl
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.4
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Dale Hamel
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-06-01 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rake
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 10.4.2
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 10.4.2
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 3.2.0
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 3.2.0
|
41
|
+
description: Terraform DSL provides a syntax for managing terraform infrastructure
|
42
|
+
entirely in git
|
43
|
+
email: dale.hamel@srvthe.net
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- lib/tasks/tasks.rb
|
49
|
+
- lib/tasks/terraform.rake
|
50
|
+
- lib/terraform_dsl.rb
|
51
|
+
- lib/terraform_dsl/aws/remote_s3.rb
|
52
|
+
- lib/terraform_dsl/aws/tagging.rb
|
53
|
+
- lib/terraform_dsl/command.rb
|
54
|
+
- lib/terraform_dsl/module.rb
|
55
|
+
- lib/terraform_dsl/output.rb
|
56
|
+
- lib/terraform_dsl/provider.rb
|
57
|
+
- lib/terraform_dsl/remote.rb
|
58
|
+
- lib/terraform_dsl/resource.rb
|
59
|
+
- lib/terraform_dsl/secrets.rb
|
60
|
+
- lib/terraform_dsl/stack.rb
|
61
|
+
- lib/terraform_dsl/stack_element.rb
|
62
|
+
- lib/terraform_dsl/stack_module.rb
|
63
|
+
- lib/terraform_dsl/stack_modules.rb
|
64
|
+
- lib/terraform_dsl/stacks.rb
|
65
|
+
- lib/terraform_dsl/variable.rb
|
66
|
+
homepage: https://rubygems.org/gems/terraform_dsl
|
67
|
+
licenses:
|
68
|
+
- MIT
|
69
|
+
metadata: {}
|
70
|
+
post_install_message:
|
71
|
+
rdoc_options: []
|
72
|
+
require_paths:
|
73
|
+
- lib
|
74
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
75
|
+
requirements:
|
76
|
+
- - ">="
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '0'
|
79
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '0'
|
84
|
+
requirements: []
|
85
|
+
rubyforge_project:
|
86
|
+
rubygems_version: 2.4.6
|
87
|
+
signing_key:
|
88
|
+
specification_version: 4
|
89
|
+
summary: Wrap hashicorps "terraform" in a ruby DSL for managing stack templates
|
90
|
+
test_files: []
|
91
|
+
has_rdoc:
|