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.
@@ -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
+ ###############################################################################