terraform-wrapper 0.0.2
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/.gitignore +28 -0
- data/.gitlab-ci.yml +35 -0
- data/Gemfile +17 -0
- data/LICENSE.txt +21 -0
- data/Rakefile +13 -0
- data/bin/console +16 -0
- data/bin/setup +18 -0
- data/lib/terraform-wrapper.rb +47 -0
- data/lib/terraform-wrapper/common.rb +38 -0
- data/lib/terraform-wrapper/shared.rb +11 -0
- data/lib/terraform-wrapper/shared/backends.rb +11 -0
- data/lib/terraform-wrapper/shared/backends/aws.rb +106 -0
- data/lib/terraform-wrapper/shared/backends/azure.rb +120 -0
- data/lib/terraform-wrapper/shared/backends/common.rb +86 -0
- data/lib/terraform-wrapper/shared/backends/local.rb +86 -0
- data/lib/terraform-wrapper/shared/binary.rb +102 -0
- data/lib/terraform-wrapper/shared/code.rb +54 -0
- data/lib/terraform-wrapper/shared/config.rb +125 -0
- data/lib/terraform-wrapper/shared/identifiers.rb +70 -0
- data/lib/terraform-wrapper/shared/latest.rb +75 -0
- data/lib/terraform-wrapper/shared/runner.rb +155 -0
- data/lib/terraform-wrapper/tasks.rb +12 -0
- data/lib/terraform-wrapper/tasks/apply.rb +68 -0
- data/lib/terraform-wrapper/tasks/binary.rb +178 -0
- data/lib/terraform-wrapper/tasks/clean.rb +141 -0
- data/lib/terraform-wrapper/tasks/destroy.rb +68 -0
- data/lib/terraform-wrapper/tasks/init.rb +67 -0
- data/lib/terraform-wrapper/tasks/plan.rb +68 -0
- data/lib/terraform-wrapper/tasks/plandestroy.rb +68 -0
- data/lib/terraform-wrapper/tasks/validate.rb +59 -0
- data/lib/terraform-wrapper/version.rb +13 -0
- data/terraform-wrapper.gemspec +36 -0
- metadata +79 -0
@@ -0,0 +1,70 @@
|
|
1
|
+
###############################################################################
|
2
|
+
|
3
|
+
module TerraformWrapper
|
4
|
+
|
5
|
+
###############################################################################
|
6
|
+
|
7
|
+
module Shared
|
8
|
+
|
9
|
+
###############################################################################
|
10
|
+
|
11
|
+
class Identifiers
|
12
|
+
|
13
|
+
###############################################################################
|
14
|
+
|
15
|
+
attr_reader :identifiers
|
16
|
+
|
17
|
+
###############################################################################
|
18
|
+
|
19
|
+
def initialize(identifiers: Hash.new, sort: true)
|
20
|
+
cleansed = cleanse(identifiers: identifiers)
|
21
|
+
@identifiers = sort ? cleansed.sort : cleansed
|
22
|
+
end
|
23
|
+
|
24
|
+
###############################################################################
|
25
|
+
|
26
|
+
def path()
|
27
|
+
result = String.new
|
28
|
+
|
29
|
+
@identifiers.each do |key, value|
|
30
|
+
directory = key + "-" + value
|
31
|
+
result = result.empty? ? directory : File.join(result, directory)
|
32
|
+
end
|
33
|
+
|
34
|
+
return result
|
35
|
+
end
|
36
|
+
|
37
|
+
###############################################################################
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
###############################################################################
|
42
|
+
|
43
|
+
def cleanse(identifiers:)
|
44
|
+
result = Hash.new
|
45
|
+
|
46
|
+
identifiers.keys.each do |key|
|
47
|
+
raise "Could not clean identifiers hash. All keys MUST be strings!" unless key.kind_of?(String)
|
48
|
+
raise "Could not clean identifiers hash, duplicate key found: #{key.downcase}!" if result.key?(key.downcase)
|
49
|
+
raise "Could not clean identifiers hash, value for: #{key.downcase} is not a string!" unless identifiers[key].kind_of?(String)
|
50
|
+
raise "Could not clean identifiers hash, value for: #{key.downcase} is empty!" if identifiers[key].strip.empty?
|
51
|
+
|
52
|
+
result[key.downcase] = identifiers[key].strip.downcase
|
53
|
+
end
|
54
|
+
|
55
|
+
return result
|
56
|
+
end
|
57
|
+
|
58
|
+
###############################################################################
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
###############################################################################
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
###############################################################################
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
###############################################################################
|
@@ -0,0 +1,75 @@
|
|
1
|
+
###############################################################################
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'net/http'
|
5
|
+
require 'singleton'
|
6
|
+
require 'uri'
|
7
|
+
|
8
|
+
###############################################################################
|
9
|
+
|
10
|
+
module TerraformWrapper
|
11
|
+
|
12
|
+
###############################################################################
|
13
|
+
|
14
|
+
module Shared
|
15
|
+
|
16
|
+
###############################################################################
|
17
|
+
|
18
|
+
class Latest
|
19
|
+
|
20
|
+
###############################################################################
|
21
|
+
|
22
|
+
include Singleton
|
23
|
+
|
24
|
+
###############################################################################
|
25
|
+
|
26
|
+
@version
|
27
|
+
|
28
|
+
###############################################################################
|
29
|
+
|
30
|
+
def version
|
31
|
+
@version ||= refresh
|
32
|
+
|
33
|
+
return @version
|
34
|
+
end
|
35
|
+
|
36
|
+
###############################################################################
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
###############################################################################
|
41
|
+
|
42
|
+
def refresh
|
43
|
+
$logger.info("Finding latest available Terraform release...")
|
44
|
+
|
45
|
+
response = Net::HTTP.get_response(URI("https://checkpoint-api.hashicorp.com/v1/check/terraform"))
|
46
|
+
|
47
|
+
raise "Hashicorp Checkpoint did not return status 200 for latest version check!" if response.code != "200"
|
48
|
+
raise "Response body from Hashicorp Checkpoint is not permitted!" if not response.class.body_permitted?
|
49
|
+
raise "Response body from Hashicorp Checkpoint is empty!" if response.body.nil?
|
50
|
+
|
51
|
+
body = JSON.parse(response.body)
|
52
|
+
|
53
|
+
raise "Hashicorp Checkpoint JSON response did not include latest available Terraform version!" if not body.key?("current_version")
|
54
|
+
raise "Hashicorp Checkpoint indicated latest available version of Terraform is blank!" if body["current_version"].empty?
|
55
|
+
|
56
|
+
version = body["current_version"]
|
57
|
+
|
58
|
+
$logger.info("Latest available Terraform release found: #{version}")
|
59
|
+
|
60
|
+
return version
|
61
|
+
end
|
62
|
+
|
63
|
+
###############################################################################
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
###############################################################################
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
###############################################################################
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
###############################################################################
|
@@ -0,0 +1,155 @@
|
|
1
|
+
###############################################################################
|
2
|
+
|
3
|
+
module TerraformWrapper
|
4
|
+
|
5
|
+
###############################################################################
|
6
|
+
|
7
|
+
module Shared
|
8
|
+
|
9
|
+
###############################################################################
|
10
|
+
|
11
|
+
class Runner
|
12
|
+
|
13
|
+
###############################################################################
|
14
|
+
|
15
|
+
attr_reader :binary
|
16
|
+
attr_reader :code
|
17
|
+
attr_reader :config
|
18
|
+
attr_reader :downloaded
|
19
|
+
attr_reader :initialised
|
20
|
+
|
21
|
+
###############################################################################
|
22
|
+
|
23
|
+
def initialize(binary:, code:)
|
24
|
+
@binary = binary
|
25
|
+
@code = code
|
26
|
+
|
27
|
+
@initialised = false
|
28
|
+
@ready = false
|
29
|
+
end
|
30
|
+
|
31
|
+
###############################################################################
|
32
|
+
|
33
|
+
def download
|
34
|
+
parameters = [ "-backend=false" ]
|
35
|
+
@downloaded = run(action: "init", parameters: parameters)
|
36
|
+
raise("Failed to download Terraform modules.") unless @downloaded
|
37
|
+
end
|
38
|
+
|
39
|
+
###############################################################################
|
40
|
+
|
41
|
+
def init(config:)
|
42
|
+
parameters = [ "-reconfigure" ]
|
43
|
+
config.backend.hash.each do |key, value|
|
44
|
+
parameters.append("-backend-config=\"#{key}=#{value}\"")
|
45
|
+
end
|
46
|
+
|
47
|
+
@config = config
|
48
|
+
@initialised = run(action: "init", parameters: parameters)
|
49
|
+
raise("Failed to initialise Terraform with backend.") unless @initialised
|
50
|
+
end
|
51
|
+
|
52
|
+
###############################################################################
|
53
|
+
|
54
|
+
def plan(destroy: false, file: nil)
|
55
|
+
raise("Cannot Terraform plan before initialising backend!") unless initialised
|
56
|
+
|
57
|
+
parameters = variable_files
|
58
|
+
|
59
|
+
if not file.nil? and file.kind_of?(String) and not file.strip.empty? then
|
60
|
+
raise "Failed to create plan directory: #{directory}" unless create_directory(directory: File.dirname(file), purpose: "plan")
|
61
|
+
parameters.append("-out=\"#{file}\"")
|
62
|
+
end
|
63
|
+
|
64
|
+
parameters.append("-destroy") if destroy
|
65
|
+
|
66
|
+
raise("Terraform plan failed!") unless run(action: "plan", parameters: parameters)
|
67
|
+
end
|
68
|
+
|
69
|
+
###############################################################################
|
70
|
+
|
71
|
+
def apply(file: nil)
|
72
|
+
raise("Cannot Terraform apply before initialising backend!") unless initialised
|
73
|
+
|
74
|
+
parameters = [ "-auto-approve" ]
|
75
|
+
|
76
|
+
if not file.nil? and file.kind_of?(String) and not file.strip.empty? then
|
77
|
+
raise "Plan file: #{file} does not exist!" unless File.file?(file)
|
78
|
+
parameters.append("\"#{file}\"")
|
79
|
+
else
|
80
|
+
parameters.concat(variable_files)
|
81
|
+
end
|
82
|
+
|
83
|
+
raise("Terraform apply failed!") unless run(action: "apply", parameters: parameters)
|
84
|
+
end
|
85
|
+
|
86
|
+
###############################################################################
|
87
|
+
|
88
|
+
def destroy
|
89
|
+
raise("Cannot Terraform destroy before initialising backend!") unless initialised
|
90
|
+
|
91
|
+
parameters = [ "-auto-approve" ]
|
92
|
+
parameters.concat(variable_files)
|
93
|
+
|
94
|
+
raise("Terraform destroy failed!") unless run(action: "destroy", parameters: parameters)
|
95
|
+
end
|
96
|
+
|
97
|
+
###############################################################################
|
98
|
+
|
99
|
+
def validate
|
100
|
+
raise("Cannot Terraform validate before downloading modules!") unless downloaded
|
101
|
+
raise("Terraform validation failed!") unless run(action: "validate")
|
102
|
+
end
|
103
|
+
|
104
|
+
###############################################################################
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
###############################################################################
|
109
|
+
|
110
|
+
def variable_files
|
111
|
+
raise("Cannot generate variable files until Terraform has been initialised!") unless @initialised
|
112
|
+
|
113
|
+
result = Array.new
|
114
|
+
|
115
|
+
@config.variable_files.each do |variable_file|
|
116
|
+
result.append("-var-file=\"#{variable_file}\"")
|
117
|
+
end
|
118
|
+
|
119
|
+
return result
|
120
|
+
end
|
121
|
+
|
122
|
+
###############################################################################
|
123
|
+
|
124
|
+
def run(action:, parameters: Array.new)
|
125
|
+
result = false
|
126
|
+
|
127
|
+
parameters.reject! { |item| not item.kind_of?(String) or item.strip.empty? }
|
128
|
+
|
129
|
+
cmdline = [ "\"#{@binary.path}\"", action ].concat(parameters).join(" ")
|
130
|
+
|
131
|
+
$logger.info("Starting Terraform, action: #{action}")
|
132
|
+
|
133
|
+
puts("\n" + ('#' * 80) + "\n\n")
|
134
|
+
|
135
|
+
Dir.chdir(@code.path)
|
136
|
+
result = system(cmdline) || false
|
137
|
+
|
138
|
+
puts("\n")
|
139
|
+
|
140
|
+
return result
|
141
|
+
end
|
142
|
+
|
143
|
+
###############################################################################
|
144
|
+
|
145
|
+
end
|
146
|
+
|
147
|
+
###############################################################################
|
148
|
+
|
149
|
+
end
|
150
|
+
|
151
|
+
###############################################################################
|
152
|
+
|
153
|
+
end
|
154
|
+
|
155
|
+
###############################################################################
|
@@ -0,0 +1,12 @@
|
|
1
|
+
###############################################################################
|
2
|
+
|
3
|
+
require_relative 'tasks/apply'
|
4
|
+
require_relative 'tasks/binary'
|
5
|
+
require_relative 'tasks/clean'
|
6
|
+
require_relative 'tasks/destroy'
|
7
|
+
require_relative 'tasks/init'
|
8
|
+
require_relative 'tasks/plan'
|
9
|
+
require_relative 'tasks/plandestroy'
|
10
|
+
require_relative 'tasks/validate'
|
11
|
+
|
12
|
+
###############################################################################
|
@@ -0,0 +1,68 @@
|
|
1
|
+
###############################################################################
|
2
|
+
|
3
|
+
require 'rake/tasklib'
|
4
|
+
|
5
|
+
###############################################################################
|
6
|
+
|
7
|
+
module TerraformWrapper
|
8
|
+
|
9
|
+
###############################################################################
|
10
|
+
|
11
|
+
module Tasks
|
12
|
+
|
13
|
+
###############################################################################
|
14
|
+
|
15
|
+
class Apply < ::Rake::TaskLib
|
16
|
+
|
17
|
+
###############################################################################
|
18
|
+
|
19
|
+
@backend
|
20
|
+
@binary
|
21
|
+
@code
|
22
|
+
@configs
|
23
|
+
@overrides
|
24
|
+
@service
|
25
|
+
|
26
|
+
###############################################################################
|
27
|
+
|
28
|
+
def initialize(backend:, binary:, code:, configs:, overrides:, service:)
|
29
|
+
@backend = backend
|
30
|
+
@binary = binary
|
31
|
+
@code = code
|
32
|
+
@configs = configs
|
33
|
+
@overrides = overrides
|
34
|
+
@service = service
|
35
|
+
|
36
|
+
yield self if block_given?
|
37
|
+
|
38
|
+
apply_task
|
39
|
+
end
|
40
|
+
|
41
|
+
###############################################################################
|
42
|
+
|
43
|
+
def apply_task
|
44
|
+
desc "Applies infrastructure with Terraform for a given configuration on a workspace."
|
45
|
+
task :apply, [:config, :plan] => :binary do |t, args|
|
46
|
+
$logger.info("Running Terraform apply for service: #{@service}, component: #{@code.name}...")
|
47
|
+
|
48
|
+
config = TerraformWrapper::Shared::Config.new(backend: @backend, base: @configs, code: @code, name: args[:config], overrides: @overrides, service: @service)
|
49
|
+
runner = TerraformWrapper::Shared::Runner.new(binary: @binary, code: @code)
|
50
|
+
|
51
|
+
runner.init(config: config)
|
52
|
+
runner.apply(file: args[:plan])
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
###############################################################################
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
###############################################################################
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
###############################################################################
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
###############################################################################
|
@@ -0,0 +1,178 @@
|
|
1
|
+
###############################################################################
|
2
|
+
|
3
|
+
require 'digest'
|
4
|
+
require 'fileutils'
|
5
|
+
require 'net/http'
|
6
|
+
require 'rake/tasklib'
|
7
|
+
require 'uri'
|
8
|
+
require 'zip'
|
9
|
+
|
10
|
+
###############################################################################
|
11
|
+
|
12
|
+
module TerraformWrapper
|
13
|
+
|
14
|
+
###############################################################################
|
15
|
+
|
16
|
+
module Tasks
|
17
|
+
|
18
|
+
###############################################################################
|
19
|
+
|
20
|
+
class Binary < ::Rake::TaskLib
|
21
|
+
|
22
|
+
###############################################################################
|
23
|
+
|
24
|
+
@binary
|
25
|
+
|
26
|
+
###############################################################################
|
27
|
+
|
28
|
+
def initialize(binary:)
|
29
|
+
@binary = binary
|
30
|
+
|
31
|
+
yield self if block_given?
|
32
|
+
|
33
|
+
binary_task
|
34
|
+
end
|
35
|
+
|
36
|
+
###############################################################################
|
37
|
+
|
38
|
+
def binary_task
|
39
|
+
desc "Downloads and extracts the expected version of the Terraform binary if it is not already present."
|
40
|
+
task :binary do |t, args|
|
41
|
+
$logger.info("Checking Terraform binary for platform: #{@binary.platform}, version: #{@binary.version}")
|
42
|
+
|
43
|
+
if not @binary.exists then
|
44
|
+
$logger.info("Terraform binary not found. Preparing binary...")
|
45
|
+
|
46
|
+
raise "Failed to create binary directory: #{directory}" unless create_directory(directory: @binary.directory, purpose: "binaries")
|
47
|
+
|
48
|
+
archive_binary = "terraform"
|
49
|
+
archive_file = "terraform_#{@binary.version}_#{@binary.platform}_amd64.zip"
|
50
|
+
archive_path = File.join(@binary.directory, archive_file)
|
51
|
+
archive_uri = "https://releases.hashicorp.com/terraform/#{@binary.version}/#{archive_file}"
|
52
|
+
|
53
|
+
sums_file = "terraform_#{@binary.version}_SHA256SUMS"
|
54
|
+
sums_path = File.join(@binary.directory, sums_file)
|
55
|
+
sums_uri = "https://releases.hashicorp.com/terraform/#{@binary.version}/#{sums_file}"
|
56
|
+
|
57
|
+
begin
|
58
|
+
download(path: archive_path, uri: archive_uri) if not File.file?(archive_path)
|
59
|
+
download(path: sums_path, uri: sums_uri) if not File.file?(sums_path)
|
60
|
+
verify(file: archive_file, path: archive_path, sums: sums_path)
|
61
|
+
extract(archive: archive_path, binary: archive_binary, destination: @binary.path)
|
62
|
+
ensure
|
63
|
+
clean(archive: archive_path, sums: sums_path)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
if not @binary.executable then
|
68
|
+
$logger.info("Terraform binary not executable. Setting permissions...")
|
69
|
+
executable(path: @binary.path)
|
70
|
+
end
|
71
|
+
|
72
|
+
raise("Problem with checking the Terraform binary!") unless @binary.check
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
###############################################################################
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
###############################################################################
|
81
|
+
|
82
|
+
def download(path:, uri:)
|
83
|
+
$logger.info("Downloading: #{uri}")
|
84
|
+
|
85
|
+
response = Net::HTTP.get_response(URI(uri))
|
86
|
+
|
87
|
+
raise "Download request did not return HTTP status 200!" if response.code != "200"
|
88
|
+
raise "Download response body is not permitted!" unless response.class.body_permitted?
|
89
|
+
raise "Download response body is empty!" if response.body.nil?
|
90
|
+
|
91
|
+
open(path, "wb") { |file|
|
92
|
+
file.write(response.body)
|
93
|
+
}
|
94
|
+
|
95
|
+
raise "Download failed!" unless File.file?(path)
|
96
|
+
end
|
97
|
+
|
98
|
+
###############################################################################
|
99
|
+
|
100
|
+
def verify(file:, path:, sums:)
|
101
|
+
$logger.info("Checking SHA256 for: #{file}")
|
102
|
+
|
103
|
+
result = false
|
104
|
+
|
105
|
+
sha256 = Digest::SHA256.hexdigest File.read(path)
|
106
|
+
|
107
|
+
File.readlines(sums).each do |line|
|
108
|
+
begin
|
109
|
+
fields = line.match /^(?<sum>\S+)\s+(?<file>\S+)$/
|
110
|
+
sum_file = fields["file"]
|
111
|
+
sum_sha256 = fields["sum"]
|
112
|
+
rescue
|
113
|
+
$logger.warn("Unexpected data in sums file: #{sums}")
|
114
|
+
next
|
115
|
+
end
|
116
|
+
|
117
|
+
if sum_file == file then
|
118
|
+
$logger.info("Expected SHA256 sum: #{sum_sha256}")
|
119
|
+
$logger.info("Actual SHA256 sum: #{sha256}")
|
120
|
+
result = (sum_sha256 == sha256)
|
121
|
+
break
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
raise "Error whilst verifying the SHA256 sum of the downloaded Terraform archive!" unless result
|
126
|
+
end
|
127
|
+
|
128
|
+
###############################################################################
|
129
|
+
|
130
|
+
def extract(archive:, binary:, destination:)
|
131
|
+
$logger.info("Extracting: #{archive}")
|
132
|
+
|
133
|
+
Zip::ZipFile.open(archive) do |zip|
|
134
|
+
zip.each do |file|
|
135
|
+
zip.extract(file, destination) if file.name == binary
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
raise "Extraction of Terraform binary: #{binary}, from archive: #{archive} has failed!" unless File.file?(destination)
|
140
|
+
end
|
141
|
+
|
142
|
+
###############################################################################
|
143
|
+
|
144
|
+
def executable(path:)
|
145
|
+
$logger.info("Making executable: #{path}")
|
146
|
+
FileUtils.chmod("+x", path)
|
147
|
+
raise "Setting executable bit on file: #{path} has failed!" unless File.executable?(path)
|
148
|
+
end
|
149
|
+
|
150
|
+
###############################################################################
|
151
|
+
|
152
|
+
def clean(archive:, sums:)
|
153
|
+
[archive, sums].each do |file|
|
154
|
+
if File.file?(file)
|
155
|
+
$logger.info("Removing file: #{file}")
|
156
|
+
|
157
|
+
begin
|
158
|
+
File.delete(file)
|
159
|
+
rescue
|
160
|
+
$logger.error("Failed to delete: #{file}, please remove manually.")
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
###############################################################################
|
167
|
+
|
168
|
+
end
|
169
|
+
|
170
|
+
###############################################################################
|
171
|
+
|
172
|
+
end
|
173
|
+
|
174
|
+
###############################################################################
|
175
|
+
|
176
|
+
end
|
177
|
+
|
178
|
+
###############################################################################
|