terraform_dsl 0.0.4
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 +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:
|