scout-camp 0.1.1
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/.vimproject +81 -0
- data/LICENSE +20 -0
- data/README.md +26 -0
- data/Rakefile +70 -0
- data/VERSION +1 -0
- data/bin/scout-camp +5 -0
- data/lib/scout/aws/s3.rb +157 -0
- data/lib/scout/offsite/exceptions.rb +9 -0
- data/lib/scout/offsite/ssh.rb +175 -0
- data/lib/scout/offsite/step.rb +100 -0
- data/lib/scout/offsite/sync.rb +55 -0
- data/lib/scout/offsite.rb +3 -0
- data/lib/scout/terraform_dsl/deployment.rb +285 -0
- data/lib/scout/terraform_dsl/util.rb +100 -0
- data/lib/scout/terraform_dsl.rb +317 -0
- data/lib/scout-camp.rb +6 -0
- data/scout_commands/offsite +30 -0
- data/scout_commands/terraform/add +78 -0
- data/scout_commands/terraform/apply +31 -0
- data/scout_commands/terraform/destroy +31 -0
- data/scout_commands/terraform/list +36 -0
- data/scout_commands/terraform/remove +39 -0
- data/scout_commands/terraform/status +33 -0
- data/share/terraform/aws/bucket/main.tf +8 -0
- data/share/terraform/aws/bucket/output.tf +3 -0
- data/share/terraform/aws/bucket/variables.tf +4 -0
- data/share/terraform/aws/cluster/main.tf +66 -0
- data/share/terraform/aws/cluster/output.tf +9 -0
- data/share/terraform/aws/cluster/variables.tf +49 -0
- data/share/terraform/aws/host/locals.tf +15 -0
- data/share/terraform/aws/host/main.tf +22 -0
- data/share/terraform/aws/host/output.tf +9 -0
- data/share/terraform/aws/host/variables.tf +67 -0
- data/share/terraform/aws/lambda/main.tf +40 -0
- data/share/terraform/aws/lambda/variables.tf +23 -0
- data/share/terraform/aws/provider/data.tf +35 -0
- data/share/terraform/aws/provider/output.tf +16 -0
- data/test/scout/aws/test_s3.rb +82 -0
- data/test/scout/offsite/test_ssh.rb +15 -0
- data/test/scout/offsite/test_step.rb +33 -0
- data/test/scout/offsite/test_sync.rb +36 -0
- data/test/scout/test_terraform_dsl.rb +519 -0
- data/test/test_helper.rb +19 -0
- metadata +99 -0
@@ -0,0 +1,55 @@
|
|
1
|
+
class SSHLine
|
2
|
+
def self.locate(server, paths, map: :user)
|
3
|
+
SSHLine.scout server, <<-EOF
|
4
|
+
map = :#{map}
|
5
|
+
paths = [#{paths.collect{|p| "'" + p + "'" } * ", " }]
|
6
|
+
located = paths.collect{|p| Path.setup(p).find(map) }
|
7
|
+
identified = paths.collect{|p| Resource.identify(p) }
|
8
|
+
[located, identified]
|
9
|
+
EOF
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.rsync(source_path, target_path, directory: false, source: nil, target: nil, dry_run: false, hard_link: false)
|
13
|
+
rsync_args = "-avztHP --copy-unsafe-links --omit-dir-times "
|
14
|
+
|
15
|
+
rsync_args << "--link-dest '#{source_path}' " if hard_link && ! source
|
16
|
+
|
17
|
+
source_path = source_path + "/" if directory && ! source_path.end_with?("/")
|
18
|
+
target_path = target_path + "/" if directory && ! target_path.end_with?("/")
|
19
|
+
if target
|
20
|
+
SSHLine.mkdir target, File.dirname(target_path)
|
21
|
+
else
|
22
|
+
Open.mkdir(File.dirname(target_path))
|
23
|
+
end
|
24
|
+
|
25
|
+
cmd = 'rsync '
|
26
|
+
cmd << rsync_args
|
27
|
+
cmd << '-nv ' if dry_run
|
28
|
+
cmd << (source ? [source, source_path] * ":" : source_path) << " "
|
29
|
+
cmd << (target ? [target, target_path] * ":" : target_path) << " "
|
30
|
+
|
31
|
+
CMD.cmd_log(cmd, :log => Log::HIGH)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.sync(paths, source: nil, target: nil, map: :user, **kwargs)
|
35
|
+
source = nil if source == 'localhost'
|
36
|
+
target = nil if target == 'localhost'
|
37
|
+
|
38
|
+
if source
|
39
|
+
source_paths, identified_paths = SSHLine.locate(source, paths)
|
40
|
+
else
|
41
|
+
source_paths = paths.collect{|p| Path === p ? p.find : p }
|
42
|
+
identified_paths = paths.collect{|p| Resource.identify(p) }
|
43
|
+
end
|
44
|
+
|
45
|
+
if target
|
46
|
+
target_paths = SSHLine.locate(target, identified_paths, map: map)
|
47
|
+
else
|
48
|
+
target_paths = identified_paths.collect{|p| p.find(map) }
|
49
|
+
end
|
50
|
+
|
51
|
+
source_paths.zip(target_paths).each do |source_path,target_path|
|
52
|
+
rsync(source_path, target_path, source: source, target: target, **kwargs)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,285 @@
|
|
1
|
+
require_relative 'util'
|
2
|
+
require 'open3'
|
3
|
+
|
4
|
+
class TerraformDSL
|
5
|
+
|
6
|
+
# Manage Terraform deployments
|
7
|
+
class Deployment
|
8
|
+
|
9
|
+
# Exception running terraform command
|
10
|
+
class TerraformException < StandardError; end
|
11
|
+
|
12
|
+
# Run a terraform command returning the STDOUT as a String.
|
13
|
+
# Forwards STDERR of the process
|
14
|
+
#
|
15
|
+
# @param cmd [String] terraform command to run (not including terraform
|
16
|
+
# command name)
|
17
|
+
# @return [String] STDOUT of the process
|
18
|
+
def self.run(cmd)
|
19
|
+
Open3.popen3("terraform #{cmd}") do |stdin, stdout, stderr, wait_thr|
|
20
|
+
TerraformDSL.log "Running: terraform #{cmd}", wait_thr.pid
|
21
|
+
stdin.close
|
22
|
+
stderr_thr = Thread.new do
|
23
|
+
while (line = stderr.gets)
|
24
|
+
TerraformDSL.log line, wait_thr.pid
|
25
|
+
end
|
26
|
+
end
|
27
|
+
out = stdout.read
|
28
|
+
exit_status = wait_thr.value
|
29
|
+
raise TerraformException, out.split(/Error:\s*/m).last if exit_status != 0
|
30
|
+
|
31
|
+
stderr_thr.join
|
32
|
+
out
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Run a terraform command loging STDERR and STDOUT of the process to STDERR
|
37
|
+
# and to a log file.
|
38
|
+
#
|
39
|
+
# @param cmd [String] terraform command to run (not including terraform
|
40
|
+
# command name)
|
41
|
+
# @param log_file [String] path to the log file (optional)
|
42
|
+
def self.run_log(cmd, log_file = nil)
|
43
|
+
log_io = Open.open(log_file, mode: 'a') if log_file
|
44
|
+
log_io.sync = true if log_io
|
45
|
+
Open3.popen3("terraform #{cmd}") do |stdin, stdout, stderr, wait_thr|
|
46
|
+
TerraformDSL.log "Running: terraform #{cmd}", wait_thr.pid
|
47
|
+
stdin.close
|
48
|
+
wait_thr.pid
|
49
|
+
stdin.close
|
50
|
+
stderr_thr = Thread.new do
|
51
|
+
while (line = stderr.gets)
|
52
|
+
TerraformDSL.log line, [wait_thr.pid, :STDERR] * " - "
|
53
|
+
log_io.puts "[#{Time.now} - STDERR]: " + line if log_io
|
54
|
+
end
|
55
|
+
end
|
56
|
+
stdout_thr = Thread.new do
|
57
|
+
while (line = stdout.gets)
|
58
|
+
TerraformDSL.log line, [wait_thr.pid, :STDOUT] * " - "
|
59
|
+
log_io.puts "[#{Time.now} - STDOUT]: " + line if log_io
|
60
|
+
end
|
61
|
+
end
|
62
|
+
exit_status = wait_thr.value
|
63
|
+
|
64
|
+
stderr_thr.join
|
65
|
+
stdout_thr.join
|
66
|
+
log_io.close if log_io
|
67
|
+
|
68
|
+
if exit_status != 0
|
69
|
+
log_io.close if log_io
|
70
|
+
log_txt = Open.read(log_file, :encoding => "UTF-8")
|
71
|
+
error_msg = log_txt.split(/Error:/).last
|
72
|
+
error_msg = error_msg.split("\n").collect{|e| e.sub(/.*? STD...\]:\s*/,'') } * "\n"
|
73
|
+
raise TerraformException, error_msg
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
nil
|
78
|
+
end
|
79
|
+
|
80
|
+
attr_accessor :directory
|
81
|
+
|
82
|
+
# Create a new deployment on a given directory.
|
83
|
+
# Templates and modules will reside on the directory and can be used by
|
84
|
+
# terraform
|
85
|
+
#
|
86
|
+
# @param config_dir [String] path to the deployment directory
|
87
|
+
def initialize(config_dir)
|
88
|
+
@directory = (Path === config_dir) ? config_dir.find : config_dir
|
89
|
+
|
90
|
+
@init = false
|
91
|
+
end
|
92
|
+
|
93
|
+
# @return [String] File where the terraform plan will be stored
|
94
|
+
def plan_file
|
95
|
+
@directory['main.plan']
|
96
|
+
end
|
97
|
+
|
98
|
+
# @return [String] File where the logs will be stored
|
99
|
+
def log_file
|
100
|
+
@directory.log
|
101
|
+
end
|
102
|
+
|
103
|
+
# Initialize deployment @directory with all the templates and modules.
|
104
|
+
# Sets @init to true and @planned to false. Removes plan_file if present
|
105
|
+
def init
|
106
|
+
Misc.in_dir @directory.find do
|
107
|
+
Deployment.run_log 'init', log_file
|
108
|
+
end
|
109
|
+
Open.rm plan_file if Open.exist?(plan_file)
|
110
|
+
@init = true
|
111
|
+
@planned = false
|
112
|
+
nil
|
113
|
+
end
|
114
|
+
|
115
|
+
# Update changes on a terraform deployment by running init, plan, and apply
|
116
|
+
def update
|
117
|
+
init
|
118
|
+
plan
|
119
|
+
apply
|
120
|
+
end
|
121
|
+
|
122
|
+
# Validate a terraform deployment. Runs init if required
|
123
|
+
def validate
|
124
|
+
init unless @init
|
125
|
+
Misc.in_dir @directory do
|
126
|
+
Deployment.run('validate')
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# Plan a terraform deployment and save it in #plan_file. Runs init if
|
131
|
+
# required. Saves the time in @planned
|
132
|
+
def plan
|
133
|
+
init unless @init
|
134
|
+
Misc.in_dir @directory.find do
|
135
|
+
Deployment.run_log("plan -out #{plan_file}", log_file)
|
136
|
+
end
|
137
|
+
@planned = Time.now
|
138
|
+
end
|
139
|
+
|
140
|
+
# Applies a terraform deployment by running the plan_file.
|
141
|
+
def apply
|
142
|
+
plan unless @planned
|
143
|
+
Misc.in_dir @directory do
|
144
|
+
Deployment.run_log("apply -auto-approve #{plan_file}", log_file)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def refresh
|
149
|
+
plan unless @planned
|
150
|
+
Misc.in_dir @directory do
|
151
|
+
Deployment.run_log('refresh', log_file)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# Lists all provisioned elements
|
156
|
+
#
|
157
|
+
# @return [Array] with names of provisioned elements
|
158
|
+
def provisioned_elements
|
159
|
+
Misc.in_dir @directory do
|
160
|
+
begin
|
161
|
+
Deployment.run('state list').split("\n")
|
162
|
+
rescue StandardError
|
163
|
+
[]
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
# Lists all provisioned elements
|
169
|
+
#
|
170
|
+
# @return [Hash] with templates organized by module type
|
171
|
+
def templates
|
172
|
+
elements = {}
|
173
|
+
@directory.glob("*.tf").each do |file|
|
174
|
+
if m = File.basename(file).match(/^([^.]+)\.([^.]+)\.tf/)
|
175
|
+
elements[m[1]] ||= []
|
176
|
+
elements[m[1]] << m[2]
|
177
|
+
end
|
178
|
+
end
|
179
|
+
elements
|
180
|
+
end
|
181
|
+
|
182
|
+
|
183
|
+
# Return the state of a provisioned element
|
184
|
+
#
|
185
|
+
# @return [String] state of the element in the original terraform format
|
186
|
+
def element_state(element)
|
187
|
+
Misc.in_dir @directory do
|
188
|
+
Deployment.run("state show '#{element}'")
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
# Destroys a provision
|
193
|
+
def destroy
|
194
|
+
Misc.in_dir @directory do
|
195
|
+
Deployment.run_log('destroy -auto-approve', log_file)
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
# Returns the outputs available for a current deployment
|
200
|
+
#
|
201
|
+
# @return [Hash] containg the output names (module.variable_name) and their values
|
202
|
+
def outputs
|
203
|
+
outputs = {}
|
204
|
+
|
205
|
+
Misc.in_dir @directory do
|
206
|
+
output_info = JSON.parse(Deployment.run('output -json'))
|
207
|
+
|
208
|
+
output_info.each do |output, info|
|
209
|
+
outputs[output] = info['value']
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
outputs
|
214
|
+
end
|
215
|
+
|
216
|
+
# Returns the value of an output for a given module in the current
|
217
|
+
# deployment
|
218
|
+
#
|
219
|
+
# @param name [String] name of the module
|
220
|
+
# @param output [String] name of the module output variable
|
221
|
+
#
|
222
|
+
# @return [Hash] containg the output names and their values
|
223
|
+
def output(name, output)
|
224
|
+
name = name.name if defined?(TerraformDSL::Module) && name.is_a?(TerraformDSL::Module)
|
225
|
+
|
226
|
+
outputs[[name, output].join('_')]
|
227
|
+
end
|
228
|
+
|
229
|
+
# Apply a deployment, run a block of code, and destroy the deployment
|
230
|
+
# afterwards
|
231
|
+
#
|
232
|
+
# @return whatever the block returns
|
233
|
+
def with_deployment
|
234
|
+
begin
|
235
|
+
apply
|
236
|
+
yield
|
237
|
+
ensure
|
238
|
+
destroy
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
# Delete an element of a deployment. Removes the definition file and the
|
243
|
+
# output file
|
244
|
+
#
|
245
|
+
# @param element [String] name of the element to destroy
|
246
|
+
def delete(element)
|
247
|
+
[element + '.tf', element + '.output.tf'].each do |file|
|
248
|
+
path = @directory[file]
|
249
|
+
Open.rm path
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
def bundle(file)
|
254
|
+
raise TerraformException, "Target bundle file is nil" if file.nil?
|
255
|
+
TerraformDSL.log "Bundle #{@directory} in #{file}", "TerraformDSL::Deployment"
|
256
|
+
Misc.in_dir @directory do
|
257
|
+
cmd = "tar cvfz '#{file}' *"
|
258
|
+
cmd += ' *.lock.hcl' if Dir.glob('*.lock.hcl').any?
|
259
|
+
cmd += ' > /dev/null'
|
260
|
+
system(cmd)
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
def with_bundle(&block)
|
265
|
+
name = 'deployment-bundle-tmp_' + rand(100000).to_s + '.tar.gz'
|
266
|
+
TmpFile.with_file nil, extension: 'deployment_bundle' do |tmpfile|
|
267
|
+
bundle(file)
|
268
|
+
yield file
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
def self.load(file, directory = nil)
|
273
|
+
directory ||= WORK_DIR[TerraformDSL.obj2digest(file)]
|
274
|
+
TerraformDSL.log "Load #{file} bundle into #{directory}", "TerraformDSL::Deployment"
|
275
|
+
Misc.in_dir directory do
|
276
|
+
`tar xvfz #{file}`
|
277
|
+
end
|
278
|
+
deployment = TerraformDSL::Deployment.new directory
|
279
|
+
deployment.refresh
|
280
|
+
deployment
|
281
|
+
end
|
282
|
+
|
283
|
+
end
|
284
|
+
|
285
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
require 'scout/path'
|
3
|
+
|
4
|
+
# rubocop: disable Style/Documentation
|
5
|
+
class TerraformDSL
|
6
|
+
|
7
|
+
# rubocop: enable Style/Documentation
|
8
|
+
|
9
|
+
|
10
|
+
# Log a message, optionally including a prefix between brakets
|
11
|
+
#
|
12
|
+
# @param msg [String] Message to log
|
13
|
+
# @param prefix [nil,String] Optional prefix to prepend
|
14
|
+
def self.log(msg, prefix = nil)
|
15
|
+
if prefix
|
16
|
+
STDOUT.puts("[#{prefix}] " + msg)
|
17
|
+
else
|
18
|
+
STDOUT.puts(msg)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Returns a md5 digest of an object based on its JSON representation
|
23
|
+
#
|
24
|
+
# @param obj [Object] object to digest
|
25
|
+
def self.obj2digest(obj)
|
26
|
+
Digest::MD5.hexdigest(obj.to_json)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Gather information about the input variables of a terraform module
|
30
|
+
#
|
31
|
+
# @param module_dir [String] path to the directory with the module template files
|
32
|
+
# @return [Hash] for each variable name holds a hash with
|
33
|
+
# description, type and default values
|
34
|
+
def self.module_variables(module_dir)
|
35
|
+
variables = {}
|
36
|
+
|
37
|
+
file = module_dir['variables.tf']
|
38
|
+
return variables unless Open.exist?(file)
|
39
|
+
|
40
|
+
name, description, type, default = nil
|
41
|
+
Open.read(file).split("\n").each do |line|
|
42
|
+
if (m = line.match(/^\s*variable\s+"([^"]*)"/))
|
43
|
+
if name
|
44
|
+
variables[name] =
|
45
|
+
{ :description => description, :type => type, :default => default }
|
46
|
+
name, description, type, default = nil
|
47
|
+
end
|
48
|
+
name = m[1].strip
|
49
|
+
elsif (m = line.match(/description\s*=\s*"(.*)"/))
|
50
|
+
description = m[1].strip
|
51
|
+
elsif (m = line.match(/type\s*=\s*(.*)/))
|
52
|
+
type = m[1].strip
|
53
|
+
elsif (m = line.match(/default\s*=\s*(.*)/))
|
54
|
+
default = begin
|
55
|
+
JSON.parse(m[1].strip)
|
56
|
+
rescue StandardError
|
57
|
+
m[1].strip
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
if name
|
63
|
+
variables[name] = { :description => description, :type => type, :default => default }
|
64
|
+
end
|
65
|
+
|
66
|
+
variables
|
67
|
+
end
|
68
|
+
|
69
|
+
# Gather information about the output variables of a terraform module
|
70
|
+
#
|
71
|
+
# @param module_dir [String] path to the directory with the module template files
|
72
|
+
# @return [Hash] for each variable name holds a hash with the description
|
73
|
+
def self.module_outputs(module_dir)
|
74
|
+
outputs = {}
|
75
|
+
|
76
|
+
module_dir = module_dir.find if Path === module_dir
|
77
|
+
file = module_dir['output.tf']
|
78
|
+
return outputs unless Open.exist?(file)
|
79
|
+
|
80
|
+
name, description, value = nil
|
81
|
+
Open.read(file).split("\n").each do |line|
|
82
|
+
if (m = line.match(/^\s*output\s+"([^"]*)"/))
|
83
|
+
if name
|
84
|
+
outputs[name] = { :description => description }
|
85
|
+
name, description, value = nil
|
86
|
+
end
|
87
|
+
name = m[1].strip
|
88
|
+
elsif (m = line.match(/description\s*=\s*"(.*)"/))
|
89
|
+
description = m[1].strip
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
if name
|
94
|
+
outputs[name] = { :description => description }
|
95
|
+
end
|
96
|
+
|
97
|
+
outputs
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|