TerraformDevKit 0.1.14 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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