TerraformDevKit 0.1.14 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,48 +2,25 @@ require 'open3'
2
2
 
3
3
  module TerraformDevKit
4
4
  class Command
5
- def self.run(cmd, directory: Dir.pwd, print_output: true, close_stdin: true)
6
- output = []
7
-
8
- Open3.popen2e(cmd, chdir: directory) do |stdin, stdout_and_stderr, thread|
9
- stdout_thread = Thread.new do
10
- process_output(stdout_and_stderr, print_output, output)
11
- end
12
-
13
- if close_stdin
14
- stdin.close
15
- else
16
- input_thread = Thread.new do
17
- loop { stdin.puts $stdin.gets }
18
- end
19
- end
5
+ def self.run(cmd, directory: Dir.pwd, print_output: true)
6
+ Open3.popen2e(cmd, chdir: directory) do |_, stdout_and_stderr, thread|
7
+ output = process_output(stdout_and_stderr, print_output)
20
8
 
21
9
  thread.join
22
- stdout_thread.join
23
- input_thread.terminate unless close_stdin
24
10
  raise "Error running command #{cmd}" unless thread.value.success?
11
+ return output
25
12
  end
26
-
27
- output
28
13
  end
29
14
 
30
15
  private_class_method
31
- def self.process_output(stdout_and_stderr, print_output, output)
32
- line = ''
33
- stdout_and_stderr.each_char do |char|
34
- $stdout.print(char) if print_output
35
- case char
36
- when "\r"
37
- next
38
- when "\n"
39
- output << line
40
- line = ''
41
- else
42
- line << char
43
- end
44
- end
16
+ def self.process_output(stream, print_output)
17
+ lines = []
45
18
 
46
- output << line unless line.empty?
19
+ until (line = stream.gets).nil?
20
+ print line if print_output
21
+ lines << line.strip
22
+ end
23
+ lines
47
24
  end
48
25
  end
49
26
  end
File without changes
File without changes
@@ -3,9 +3,10 @@ require 'aws-sdk'
3
3
  Aws.use_bundled_cert!
4
4
 
5
5
  module TerraformDevKit
6
+ # Wrapper class around aws dynamodb
6
7
  class DynamoDB
7
8
  def initialize(credentials, region)
8
- @db_client = Aws::DynamoDB::Resource.new(
9
+ @db_client = Aws::DynamoDB::Client.new(
9
10
  credentials: credentials,
10
11
  region: region
11
12
  )
@@ -20,5 +21,30 @@ module TerraformDevKit
20
21
  table = @db_client.table(table_name)
21
22
  table.put_item(item: item)
22
23
  end
24
+
25
+ def create_table(table_name, attributes, keys, read_capacity, write_capacity)
26
+ @db_client.create_table(
27
+ attribute_definitions: attributes,
28
+ key_schema: keys,
29
+ provisioned_throughput: {
30
+ read_capacity_units: read_capacity,
31
+ write_capacity_units: write_capacity
32
+ },
33
+ table_name: table_name
34
+ )
35
+ end
36
+
37
+ def get_table_status(table_name)
38
+ resp = @db_client.describe_table({
39
+ table_name: table_name,
40
+ })
41
+ resp.table.table_status
42
+ end
43
+
44
+ def delete_table(table_name)
45
+ @db_client.delete_table({
46
+ table_name: table_name,
47
+ })
48
+ end
23
49
  end
24
50
  end
File without changes
File without changes
File without changes
File without changes
@@ -0,0 +1,43 @@
1
+ require 'aws-sdk'
2
+
3
+ Aws.use_bundled_cert!
4
+
5
+ module TerraformDevKit
6
+ # Wrapper class around aws s3
7
+ class S3
8
+ def initialize(credentials, region)
9
+ @s3_client = Aws::S3::Client.new(
10
+ credentials: credentials,
11
+ region: region
12
+ )
13
+ end
14
+
15
+ def create_bucket(bucket_name)
16
+ @s3_client.create_bucket(
17
+ bucket: bucket_name
18
+ )
19
+ end
20
+
21
+ def delete_bucket(bucket_name)
22
+ empty_bucket(bucket_name)
23
+
24
+ @s3_client.delete_bucket(
25
+ bucket: bucket_name
26
+ )
27
+ end
28
+
29
+ def empty_bucket(bucket_name)
30
+ keys_to_delete = @s3_client
31
+ .list_objects_v2(bucket: bucket_name)
32
+ .contents
33
+ .map { |x| { key: x.key } }
34
+
35
+ @s3_client.delete_objects(
36
+ bucket: bucket_name,
37
+ delete: {
38
+ objects: keys_to_delete
39
+ }
40
+ )
41
+ end
42
+ end
43
+ end
@@ -1,6 +1,6 @@
1
- require 'TerraformDevKit/terraform_template_config_file'
2
-
3
1
  require 'fileutils'
2
+ require 'TerraformDevKit/terraform_project_config'
3
+ require 'TerraformDevKit/terraform_template_config_file'
4
4
 
5
5
  module TerraformDevKit
6
6
  class TerraformConfigManager
@@ -10,10 +10,10 @@ module TerraformDevKit
10
10
  @extra_vars_proc = p
11
11
  end
12
12
 
13
- def self.setup(env)
13
+ def self.setup(env, project)
14
14
  fix_configuration(env)
15
15
  create_environment_directory(env)
16
- render_template_config_files(env)
16
+ render_template_config_files(env, project)
17
17
  end
18
18
 
19
19
  def self.update_modules?
@@ -42,12 +42,13 @@ module TerraformDevKit
42
42
  end
43
43
 
44
44
  private_class_method
45
- def self.render_template_config_files(env)
45
+ def self.render_template_config_files(env, project)
46
46
  aws_config = Configuration.get('aws')
47
47
  file_list = Dir['*.tf.mustache'] + Dir['*.tfvars.mustache']
48
48
  file_list.each do |fname|
49
49
  template_file = TerraformTemplateConfigFile.new(
50
50
  File.read(fname),
51
+ project,
51
52
  env,
52
53
  aws_config,
53
54
  extra_vars: @extra_vars_proc.call(env)
File without changes
File without changes
File without changes
@@ -0,0 +1,10 @@
1
+ module TerraformDevKit
2
+ class TerraformProjectConfig
3
+ attr_reader :name, :acronym
4
+
5
+ def initialize(project_name)
6
+ @name = project_name.gsub(' ', '-').downcase
7
+ @acronym = project_name.scan(/\b[a-z]/i).join.upcase
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,65 @@
1
+ require 'aws-sdk'
2
+
3
+ module TerraformDevKit
4
+ # Represents a terraform lock table.
5
+ class TerraformRemoteState
6
+ ATTRIBUTES = [
7
+ {
8
+ attribute_name: 'LockID',
9
+ attribute_type: 'S'
10
+ }
11
+ ]
12
+ KEYS = [
13
+ {
14
+ attribute_name: 'LockID',
15
+ key_type: 'HASH'
16
+ }
17
+ ]
18
+
19
+ def initialize(dynamodb, s3)
20
+ @dynamodb = dynamodb
21
+ @s3 = s3
22
+ end
23
+
24
+ def init(environment, project)
25
+ table_name = table_name(environment, project)
26
+ return if lock_table_exists_and_is_active(table_name)
27
+
28
+ @dynamodb.create_table(table_name, ATTRIBUTES, KEYS, 1, 1)
29
+
30
+ begin
31
+ @s3.create_bucket(state_bucket_name(environment, project))
32
+ rescue Aws::S3::Errors::BucketAlreadyOwnedByYou
33
+ return
34
+ end
35
+
36
+ sleep(0.2) until lock_table_exists_and_is_active(table_name)
37
+ end
38
+
39
+ def destroy(environment, project)
40
+ table_name = table_name(environment, project)
41
+
42
+ @dynamodb.delete_table(table_name)
43
+ @s3.delete_bucket(state_bucket_name(environment, project))
44
+ end
45
+
46
+ private_class_method
47
+ def lock_table_exists_and_is_active(table_name)
48
+ begin
49
+ return @dynamodb.get_table_status(table_name) == 'ACTIVE'
50
+ rescue Aws::DynamoDB::Errors::ResourceNotFoundException
51
+ return false
52
+ end
53
+ end
54
+
55
+ private_class_method
56
+ def table_name(environment, project)
57
+ "#{project.acronym}-#{environment.name}-lock-table"
58
+ end
59
+
60
+ private_class_method
61
+ def state_bucket_name(environment, project)
62
+ "#{project.name}-#{environment.name}-state"
63
+ end
64
+ end
65
+ end
@@ -2,8 +2,9 @@ require 'mustache'
2
2
 
3
3
  module TerraformDevKit
4
4
  class TerraformTemplateConfigFile
5
- def initialize(content, env, aws_config, extra_vars: {})
5
+ def initialize(content, project, env, aws_config, extra_vars: {})
6
6
  @content = content
7
+ @project = project
7
8
  @env = env
8
9
  @aws_config = aws_config
9
10
  @extra_vars = extra_vars
@@ -14,7 +15,9 @@ module TerraformDevKit
14
15
  Profile: @aws_config.fetch('profile', ''),
15
16
  Region: @aws_config.fetch('region'),
16
17
  Environment: @env.name,
17
- LocalBackend: @env.local_backend?
18
+ LocalBackend: @env.local_backend?,
19
+ ProjectName: @project.name,
20
+ ProjectAcronym: @project.acronym
18
21
  }
19
22
  args.merge!(@extra_vars)
20
23
  Mustache.render(
File without changes
@@ -1,3 +1,3 @@
1
- module TerraformDevKit
2
- VERSION = '0.1.14'.freeze
3
- end
1
+ module TerraformDevKit
2
+ VERSION = '0.2.0'.freeze
3
+ end
@@ -1,47 +1,47 @@
1
- require 'zip'
2
-
3
- module TerraformDevKit
4
- class ZipFileGenerator
5
- def initialize(input_dir, output_file)
6
- @input_dir = input_dir
7
- @output_file = output_file
8
- end
9
-
10
- def write
11
- entries = Dir.entries(@input_dir)
12
- entries.delete('.')
13
- entries.delete('..')
14
- Zip::File.open(@output_file, Zip::File::CREATE) do |zipfile|
15
- write_entries(entries, '', zipfile)
16
- end
17
- end
18
-
19
- private
20
-
21
- def write_entries(entries, path, zipfile)
22
- entries.each do |e|
23
- zip_file_path = path == '' ? e : File.join(path, e)
24
- disk_file_path = File.join(@input_dir, zip_file_path)
25
- if File.directory?(disk_file_path)
26
- write_directory(disk_file_path, zip_file_path, zipfile)
27
- else
28
- write_file(disk_file_path, zip_file_path, zipfile)
29
- end
30
- end
31
- end
32
-
33
- def write_directory(disk_file_path, zip_file_path, zipfile)
34
- zipfile.mkdir(zip_file_path)
35
- subdir = Dir.entries(disk_file_path)
36
- subdir.delete('.')
37
- subdir.delete('..')
38
- write_entries(subdir, zip_file_path, zipfile)
39
- end
40
-
41
- def write_file(disk_file_path, zip_file_path, zipfile)
42
- zipfile.get_output_stream(zip_file_path) do |f|
43
- f.puts(File.open(disk_file_path, 'rb').read)
44
- end
45
- end
46
- end
47
- end
1
+ require 'zip'
2
+
3
+ module TerraformDevKit
4
+ class ZipFileGenerator
5
+ def initialize(input_dir, output_file)
6
+ @input_dir = input_dir
7
+ @output_file = output_file
8
+ end
9
+
10
+ def write
11
+ entries = Dir.entries(@input_dir)
12
+ entries.delete('.')
13
+ entries.delete('..')
14
+ Zip::File.open(@output_file, Zip::File::CREATE) do |zipfile|
15
+ write_entries(entries, '', zipfile)
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def write_entries(entries, path, zipfile)
22
+ entries.each do |e|
23
+ zip_file_path = path == '' ? e : File.join(path, e)
24
+ disk_file_path = File.join(@input_dir, zip_file_path)
25
+ if File.directory?(disk_file_path)
26
+ write_directory(disk_file_path, zip_file_path, zipfile)
27
+ else
28
+ write_file(disk_file_path, zip_file_path, zipfile)
29
+ end
30
+ end
31
+ end
32
+
33
+ def write_directory(disk_file_path, zip_file_path, zipfile)
34
+ zipfile.mkdir(zip_file_path)
35
+ subdir = Dir.entries(disk_file_path)
36
+ subdir.delete('.')
37
+ subdir.delete('..')
38
+ write_entries(subdir, zip_file_path, zipfile)
39
+ end
40
+
41
+ def write_file(disk_file_path, zip_file_path, zipfile)
42
+ zipfile.get_output_stream(zip_file_path) do |f|
43
+ f.puts(File.open(disk_file_path, 'rb').read)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -1,17 +1,19 @@
1
- require 'TerraformDevKit/aws'
2
- require 'TerraformDevKit/backup_state'
3
- require 'TerraformDevKit/command'
4
- require 'TerraformDevKit/config'
5
- require 'TerraformDevKit/dynamodb'
6
- require 'TerraformDevKit/environment'
7
- require 'TerraformDevKit/os'
8
- require 'TerraformDevKit/request'
9
- require 'TerraformDevKit/retry'
10
- require 'TerraformDevKit/terraform_config_manager'
11
- require 'TerraformDevKit/terraform_env_manager'
12
- require 'TerraformDevKit/terraform_installer'
13
- require 'TerraformDevKit/terraform_log_filter'
14
- require 'TerraformDevKit/terraform_template_config_file'
15
- require 'TerraformDevKit/terragrunt_installer'
16
- require 'TerraformDevKit/url'
17
- require 'TerraformDevKit/version'
1
+ require 'TerraformDevKit/aws'
2
+ require 'TerraformDevKit/backup_state'
3
+ require 'TerraformDevKit/command'
4
+ require 'TerraformDevKit/config'
5
+ require 'TerraformDevKit/dynamodb'
6
+ require 'TerraformDevKit/environment'
7
+ require 'TerraformDevKit/os'
8
+ require 'TerraformDevKit/request'
9
+ require 'TerraformDevKit/retry'
10
+ require 'TerraformDevKit/s3'
11
+ require 'TerraformDevKit/terraform_config_manager'
12
+ require 'TerraformDevKit/terraform_env_manager'
13
+ require 'TerraformDevKit/terraform_installer'
14
+ require 'TerraformDevKit/terraform_remote_state'
15
+ require 'TerraformDevKit/terraform_log_filter'
16
+ require 'TerraformDevKit/terraform_project_config'
17
+ require 'TerraformDevKit/terraform_template_config_file'
18
+ require 'TerraformDevKit/url'
19
+ require 'TerraformDevKit/version'
data/tasks/devkit.rake CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'fileutils'
2
+ require 'rainbow'
2
3
  require 'TerraformDevKit'
3
4
 
4
5
  TDK = TerraformDevKit
@@ -6,12 +7,14 @@ TDK = TerraformDevKit
6
7
  raise 'ROOT_PATH is not defined' if defined?(ROOT_PATH).nil?
7
8
  BIN_PATH = File.join(ROOT_PATH, 'bin')
8
9
 
9
- # Ensure terraform and terragrunt are in the PATH
10
+ # Ensure terraform is in the PATH
10
11
  ENV['PATH'] = TDK::OS.join_env_path(
11
12
  TDK::OS.convert_to_local_path(BIN_PATH),
12
13
  ENV['PATH']
13
14
  )
14
15
 
16
+ PLAN_FILE = 'plan.tfplan'.freeze
17
+
15
18
  def destroy_if_fails(env)
16
19
  yield
17
20
  rescue StandardError => e
@@ -22,9 +25,20 @@ rescue StandardError => e
22
25
  end
23
26
 
24
27
  def invoke_if_defined(task_name, env)
25
- if Rake::Task.task_defined?(task_name)
26
- task(task_name).invoke(env)
27
- end
28
+ task(task_name).invoke(env) if Rake::Task.task_defined?(task_name)
29
+ end
30
+
31
+ def remote_state
32
+ aws_config = TDK::AwsConfig.new(TDK::Configuration.get('aws'))
33
+ dynamo_db = TDK::DynamoDB.new(
34
+ aws_config.credentials,
35
+ aws_config.region
36
+ )
37
+ s3 = TDK::S3.new(
38
+ aws_config.credentials,
39
+ aws_config.region
40
+ )
41
+ TDK::TerraformRemoteState.new(dynamo_db, s3)
28
42
  end
29
43
 
30
44
  desc 'Prepares the environment to create the infrastructure'
@@ -40,30 +54,35 @@ task :prepare, [:env] do |_, args|
40
54
  TDK::Configuration.get('terraform-version'),
41
55
  directory: BIN_PATH
42
56
  )
43
- TDK::TerragruntInstaller.install_local(
44
- TDK::Configuration.get('terragrunt-version'),
45
- directory: BIN_PATH
57
+
58
+ project_config = TDK::TerraformProjectConfig.new(
59
+ TDK::Configuration.get('project-name')
46
60
  )
61
+ TDK::TerraformConfigManager.setup(env, project_config)
47
62
 
48
- TDK::TerraformConfigManager.setup(env)
63
+ unless env.local_backend?
64
+ puts '== Initializing remote state'
65
+ remote_state.init(env, project_config)
66
+ end
49
67
 
50
68
  invoke_if_defined('custom_prepare', args.env)
51
69
 
52
- TDK::Command.run(
53
- 'terragrunt init -upgrade=false',
54
- directory: env.working_dir,
55
- close_stdin: false
56
- )
70
+ if File.exist?(File.join(env.working_dir, '.terraform'))
71
+ get_cmd = 'terraform get'
72
+ get_cmd += ' -update=true' if TDK::TerraformConfigManager.update_modules?
73
+ TDK::Command.run(get_cmd, directory: env.working_dir)
74
+ else
75
+ init_cmd = 'terraform init'
76
+ init_cmd += ' -upgrade=false' unless TDK::TerraformConfigManager.update_modules?
57
77
 
58
- cmd = 'terragrunt get'
59
- cmd += ' -update=true' if TDK::TerraformConfigManager.update_modules?
60
- TDK::Command.run(cmd, directory: env.working_dir)
78
+ TDK::Command.run(init_cmd, directory: env.working_dir)
79
+ end
61
80
  end
62
81
 
63
82
  desc 'Shows the plan to create the infrastructure'
64
83
  task :plan, [:env] => :prepare do |_, args|
65
84
  env = TDK::Environment.new(args.env)
66
- TDK::Command.run('terragrunt plan', directory: env.working_dir)
85
+ TDK::Command.run("terraform plan -out=#{PLAN_FILE}", directory: env.working_dir)
67
86
  end
68
87
 
69
88
  desc 'Creates the infrastructure'
@@ -71,8 +90,21 @@ task :apply, [:env] => :prepare do |_, args|
71
90
  invoke_if_defined('pre_apply', args.env)
72
91
 
73
92
  env = TDK::Environment.new(args.env)
93
+
94
+ task('plan').invoke(env.name)
95
+
96
+ unless env.local_backend?
97
+ puts Rainbow("Are you sure you want to apply the above plan?\n" \
98
+ "Only 'yes' will be accepted.").green
99
+ response = STDIN.gets.strip
100
+ unless response == 'yes'
101
+ raise "Apply cancelled because response was not 'yes'.\n" \
102
+ "Response was: #{response}"
103
+ end
104
+ end
105
+
74
106
  destroy_if_fails(env) do
75
- TDK::Command.run('terragrunt apply', directory: env.working_dir)
107
+ TDK::Command.run("terraform apply \"#{PLAN_FILE}\"", directory: env.working_dir)
76
108
  end
77
109
 
78
110
  invoke_if_defined('post_apply', args.env)
@@ -83,7 +115,7 @@ task :test, [:env] do |_, args|
83
115
  env = TDK::Environment.new(args.env)
84
116
  env.local_backend? || (raise 'Testing is only allowed for local environments')
85
117
 
86
- task('apply').invoke(env.name)
118
+ task('apply').invoke(env.name, true)
87
119
 
88
120
  destroy_if_fails(env) do
89
121
  invoke_if_defined('custom_test', args.env)
@@ -103,9 +135,32 @@ task :destroy, [:env] => :prepare do |_, args|
103
135
  invoke_if_defined('pre_destroy', args.env)
104
136
 
105
137
  env = TDK::Environment.new(args.env)
106
- cmd = 'terragrunt destroy'
107
- cmd += ' -force' if env.local_backend?
108
- TDK::Command.run(cmd, directory: env.working_dir, close_stdin: false)
138
+ cmd = 'terraform destroy'
139
+
140
+ unless env.local_backend?
141
+ puts Rainbow("\n\n!!!! WARNING !!!!\n\n" \
142
+ "You are about to destroy #{env.name} and its remote state.\n" \
143
+ "Are you sure you want to proceed?\n" \
144
+ "Only 'yes' will be accepted.").red.bright
145
+ response = STDIN.gets.strip
146
+
147
+ unless response == 'yes'
148
+ raise "Destroy cancelled because response was not 'yes'.\n" \
149
+ "Response was: #{response}"
150
+ end
151
+ end
152
+
153
+ cmd += ' -force'
154
+
155
+ TDK::Command.run(cmd, directory: env.working_dir)
156
+ invoke_if_defined('pre_destroy', args.env)
157
+
158
+ unless env.local_backend?
159
+ project_config = TDK::TerraformProjectConfig.new(
160
+ TDK::Configuration.get('project-name')
161
+ )
162
+ remote_state.destroy(env, project_config)
163
+ end
109
164
 
110
165
  invoke_if_defined('post_destroy', args.env)
111
166
  end