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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.vimproject +81 -0
  3. data/LICENSE +20 -0
  4. data/README.md +26 -0
  5. data/Rakefile +70 -0
  6. data/VERSION +1 -0
  7. data/bin/scout-camp +5 -0
  8. data/lib/scout/aws/s3.rb +157 -0
  9. data/lib/scout/offsite/exceptions.rb +9 -0
  10. data/lib/scout/offsite/ssh.rb +175 -0
  11. data/lib/scout/offsite/step.rb +100 -0
  12. data/lib/scout/offsite/sync.rb +55 -0
  13. data/lib/scout/offsite.rb +3 -0
  14. data/lib/scout/terraform_dsl/deployment.rb +285 -0
  15. data/lib/scout/terraform_dsl/util.rb +100 -0
  16. data/lib/scout/terraform_dsl.rb +317 -0
  17. data/lib/scout-camp.rb +6 -0
  18. data/scout_commands/offsite +30 -0
  19. data/scout_commands/terraform/add +78 -0
  20. data/scout_commands/terraform/apply +31 -0
  21. data/scout_commands/terraform/destroy +31 -0
  22. data/scout_commands/terraform/list +36 -0
  23. data/scout_commands/terraform/remove +39 -0
  24. data/scout_commands/terraform/status +33 -0
  25. data/share/terraform/aws/bucket/main.tf +8 -0
  26. data/share/terraform/aws/bucket/output.tf +3 -0
  27. data/share/terraform/aws/bucket/variables.tf +4 -0
  28. data/share/terraform/aws/cluster/main.tf +66 -0
  29. data/share/terraform/aws/cluster/output.tf +9 -0
  30. data/share/terraform/aws/cluster/variables.tf +49 -0
  31. data/share/terraform/aws/host/locals.tf +15 -0
  32. data/share/terraform/aws/host/main.tf +22 -0
  33. data/share/terraform/aws/host/output.tf +9 -0
  34. data/share/terraform/aws/host/variables.tf +67 -0
  35. data/share/terraform/aws/lambda/main.tf +40 -0
  36. data/share/terraform/aws/lambda/variables.tf +23 -0
  37. data/share/terraform/aws/provider/data.tf +35 -0
  38. data/share/terraform/aws/provider/output.tf +16 -0
  39. data/test/scout/aws/test_s3.rb +82 -0
  40. data/test/scout/offsite/test_ssh.rb +15 -0
  41. data/test/scout/offsite/test_step.rb +33 -0
  42. data/test/scout/offsite/test_sync.rb +36 -0
  43. data/test/scout/test_terraform_dsl.rb +519 -0
  44. data/test/test_helper.rb +19 -0
  45. 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,3 @@
1
+ require_relative 'offsite/ssh'
2
+ require_relative 'offsite/step'
3
+ require_relative 'offsite/sync'
@@ -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