terraspace 0.3.6 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -0
  3. data/lib/templates/base/project/README.md +1 -1
  4. data/lib/terraspace/all/runner.rb +1 -0
  5. data/lib/terraspace/all/summary.rb +8 -1
  6. data/lib/terraspace/app.rb +9 -5
  7. data/lib/terraspace/builder.rb +10 -6
  8. data/lib/terraspace/cli.rb +10 -16
  9. data/lib/terraspace/cli/all.rb +6 -0
  10. data/lib/terraspace/cli/bundle.rb +2 -1
  11. data/lib/terraspace/cli/clean.rb +18 -6
  12. data/lib/terraspace/cli/clean/all.rb +18 -0
  13. data/lib/terraspace/cli/clean/base.rb +15 -0
  14. data/lib/terraspace/cli/clean/cache.rb +25 -0
  15. data/lib/terraspace/cli/{logs/tasks.rb → clean/logs.rb} +8 -9
  16. data/lib/terraspace/cli/help/clean/all.md +10 -0
  17. data/lib/terraspace/cli/help/clean/cache.md +12 -0
  18. data/lib/terraspace/cli/help/clean/logs.md +17 -0
  19. data/lib/terraspace/cli/help/{log.md → logs.md} +14 -14
  20. data/lib/terraspace/cli/init.rb +3 -7
  21. data/lib/terraspace/cli/logs.rb +105 -10
  22. data/lib/terraspace/cli/{log → logs}/concern.rb +1 -1
  23. data/lib/terraspace/cli/new/helper.rb +9 -2
  24. data/lib/terraspace/dependency/helper/output.rb +1 -1
  25. data/lib/terraspace/hooks/builder.rb +52 -0
  26. data/lib/terraspace/hooks/concern.rb +9 -0
  27. data/lib/terraspace/{terraform/hooks → hooks}/dsl.rb +3 -2
  28. data/lib/terraspace/hooks/runner.rb +23 -0
  29. data/lib/terraspace/mod.rb +11 -2
  30. data/lib/terraspace/plugin/summary/interface.rb +3 -1
  31. data/lib/terraspace/shell.rb +15 -10
  32. data/lib/terraspace/terraform/args/custom.rb +1 -1
  33. data/lib/terraspace/terraform/remote_state/output_proxy.rb +3 -3
  34. data/lib/terraspace/terraform/remote_state/{null_object.rb → unresolved.rb} +1 -1
  35. data/lib/terraspace/terraform/runner.rb +2 -7
  36. data/lib/terraspace/version.rb +1 -1
  37. data/spec/terraspace/{terraform/hooks → hooks}/builder_spec.rb +4 -5
  38. data/spec/terraspace/terraform/remote_state/output_proxy_spec.rb +3 -3
  39. data/terraspace.gemspec +1 -1
  40. metadata +20 -14
  41. data/lib/terraspace/cli/help/clean.md +0 -5
  42. data/lib/terraspace/cli/log.rb +0 -112
  43. data/lib/terraspace/terraform/hooks/builder.rb +0 -40
@@ -67,7 +67,7 @@ class Terraspace::CLI
67
67
  mode = ENV['TS_INIT_MODE'] || Terraspace.config.init.mode
68
68
  case mode.to_sym
69
69
  when :auto
70
- !already_initialized?
70
+ !already_init?
71
71
  when :always
72
72
  true
73
73
  when :never
@@ -80,16 +80,12 @@ class Terraspace::CLI
80
80
  # Traverse symlink dirs also: linux_amd64 is a symlink
81
81
  # plugins/registry.terraform.io/hashicorp/google/3.39.0/linux_amd64/terraform-provider-google_v3.39.0_x5
82
82
  #
83
- # Check modules/modules.json also because during the tfvars dependency pass main.tf modules are not built.
84
- # So init happens again during the second pass.
85
- #
86
- def already_initialized?
83
+ def already_init?
87
84
  terraform = "#{@mod.cache_dir}/.terraform"
88
85
  provider = Dir.glob("#{terraform}/**{,/*/**}/*").find do |path|
89
86
  path.include?("terraform-provider-")
90
87
  end
91
- modules = File.exist?("#{terraform}/modules/modules.json")
92
- !!(provider && modules)
88
+ !!provider
93
89
  end
94
90
  end
95
91
  end
@@ -1,17 +1,112 @@
1
+ require "eventmachine"
2
+ require "eventmachine-tail"
3
+
1
4
  class Terraspace::CLI
2
- class Logs < Terraspace::Command
3
- class_option :yes, aliases: :y, type: :boolean, desc: "bypass are you sure prompt"
5
+ class Logs < Base
6
+ include Concern
7
+
8
+ def initialize(options={})
9
+ super
10
+ @action, @stack = options[:action], options[:stack]
11
+ @action ||= '**'
12
+ @stack ||= '*'
13
+ end
14
+
15
+ def run
16
+ check_logs!
17
+ if @options[:follow]
18
+ follow_logs
19
+ else
20
+ all_log_paths.each { |path| show_log(path) }
21
+ end
22
+ end
23
+
24
+ def follow_logs
25
+ glob_path = "#{Terraspace.log_root}/#{@action}/#{@stack}.log"
26
+ Dir.glob(glob_path).each do |path|
27
+ puts "Following #{pretty(path)}".color(:purple)
28
+ end
29
+ EventMachine.run do
30
+ interval = Integer(ENV['TS_LOG_GLOB_INTERNAL'] || 1)
31
+ EventMachine::FileGlobWatchTail.new(glob_path, nil, interval) do |filetail, line|
32
+ puts line # always show timestamp in follow mode
33
+ end
34
+ end
35
+ end
36
+
37
+ def show_log(path)
38
+ report_log(path)
39
+ lines = readlines(path)
40
+ lines = apply_limit(lines)
41
+ lines.each do |line|
42
+ puts format(line)
43
+ end
44
+ end
45
+
46
+ def report_log(path)
47
+ pretty_path = pretty(path)
48
+ if File.exist?(path)
49
+ puts "Showing: #{pretty_path}".color(:purple)
50
+ end
51
+ end
52
+
53
+ def format(line)
54
+ if timestamps
55
+ line
56
+ else
57
+ line.sub(/.*\]: /,'')
58
+ end
59
+ end
4
60
 
5
- desc "truncate", "Truncates logs. IE: Keeps the files but removes contents and zero bytes the files."
6
- long_desc Help.text("logs/truncate")
7
- def truncate
8
- Tasks.new(options).truncate
61
+ def all_log_paths
62
+ Dir.glob("#{Terraspace.log_root}/#{@action}/#{@stack}.log")
9
63
  end
10
64
 
11
- desc "remove", "Removes logs"
12
- long_desc Help.text("logs/remove")
13
- def remove
14
- Tasks.new(options).remove
65
+ def check_logs!
66
+ return unless all_log_paths.empty?
67
+ puts "WARN: No logs found".color(:yellow)
68
+ end
69
+
70
+ # Only need to check if both action and stack are provided. Otherwise the Dir.globs are used to discover the files
71
+ def check_log!
72
+ return unless single_log?
73
+ path = "#{Terraspace.log_root}/#{@action}/#{@stack}.log"
74
+ return if File.exist?(path)
75
+ puts "ERROR: Log file was not found: #{pretty(path)}".color(:red)
76
+ exit 1
77
+ end
78
+
79
+ def single_log?
80
+ @action != '**' && @stack != '*'
81
+ end
82
+
83
+ def apply_limit(lines)
84
+ return lines if all
85
+ left = limit * -1
86
+ lines[left..-1] || []
87
+ end
88
+
89
+ def all
90
+ if single_log?
91
+ @options[:all].nil? ? true : @options[:all]
92
+ else # multiple
93
+ @options[:all].nil? ? false : @options[:all]
94
+ end
95
+ end
96
+
97
+ def limit
98
+ @options[:limit].nil? ? 10 : @options[:limit]
99
+ end
100
+
101
+ def timestamps
102
+ if single_log?
103
+ @options[:timestamps].nil? ? false : @options[:timestamps]
104
+ else
105
+ @options[:timestamps].nil? ? true : @options[:timestamps]
106
+ end
107
+ end
108
+ def pretty(path)
109
+ Terraspace::Util.pretty_path(path)
15
110
  end
16
111
  end
17
112
  end
@@ -1,4 +1,4 @@
1
- class Terraspace::CLI::Log
1
+ class Terraspace::CLI::Logs
2
2
  module Concern
3
3
  # Filters for lines that belong to the last ran process pid
4
4
  def readlines(path)
@@ -6,10 +6,17 @@ class Terraspace::CLI::New
6
6
  def build_gemfile(*list)
7
7
  lines = []
8
8
  list.each do |name|
9
- line = %Q|gem "#{name}"|
10
- lines << line
9
+ lines << gem_line(name)
11
10
  end
12
11
  lines.join("\n")
13
12
  end
13
+
14
+ def gem_line(name)
15
+ if name == "terraspace"
16
+ %Q|gem "#{name}", '~> #{Terraspace::VERSION}'|
17
+ else
18
+ %Q|gem "#{name}"|
19
+ end
20
+ end
14
21
  end
15
22
  end
@@ -4,7 +4,7 @@ module Terraspace::Dependency::Helper
4
4
  if @mod.resolved # dependencies have been resolved
5
5
  Terraspace::Terraform::RemoteState::Fetcher.new(@mod, @identifier, @options).output # Returns OutputProxy which defaults to json
6
6
  else
7
- Terraspace::Terraform::RemoteState::Marker::Output.new(@mod, @identifier, @options).build # Returns OutputProxy is NullObject when unresolved
7
+ Terraspace::Terraform::RemoteState::Marker::Output.new(@mod, @identifier, @options).build # Returns OutputProxy => Unresolved
8
8
  end
9
9
  end
10
10
  end
@@ -0,0 +1,52 @@
1
+ module Terraspace::Hooks
2
+ class Builder
3
+ extend Memoist
4
+ include Dsl
5
+ include DslEvaluator
6
+ include Terraspace::Util
7
+
8
+ # IE: dsl_file: config/hooks/terraform.rb
9
+ attr_accessor :name
10
+ def initialize(mod, dsl_file, name)
11
+ @mod, @dsl_file, @name = mod, dsl_file, name
12
+ @hooks = {before: {}, after: {}}
13
+ end
14
+
15
+ def build
16
+ return @hooks unless File.exist?(@dsl_file)
17
+ evaluate_file(@dsl_file)
18
+ @hooks.deep_stringify_keys!
19
+ end
20
+ memoize :build
21
+
22
+ def run_hooks
23
+ build
24
+ run_each_hook("before")
25
+ out = yield if block_given?
26
+ run_each_hook("after")
27
+ out
28
+ end
29
+
30
+ def run_each_hook(type)
31
+ hooks = @hooks.dig(type, @name) || []
32
+ hooks.each do |hook|
33
+ run_hook(type, hook)
34
+ end
35
+ end
36
+
37
+ def run_hook(type, hook)
38
+ return unless run?(hook)
39
+
40
+ command = File.basename(@dsl_file).sub('.rb','') # IE: kubes, kubectl, docker
41
+ id = "#{command} #{type} #{@name}"
42
+ label = " label: #{hook["label"]}" if hook["label"]
43
+ logger.info "Running #{id} hook.#{label}"
44
+ logger.debug "Hook options: #{hook}"
45
+ Runner.new(@mod, hook).run
46
+ end
47
+
48
+ def run?(hook)
49
+ !!hook["execute"]
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,9 @@
1
+ module Terraspace::Hooks
2
+ module Concern
3
+ def run_hooks(dsl_file, name, &block)
4
+ hooks = Builder.new(@mod, "#{Terraspace.root}/config/hooks/#{dsl_file}", name)
5
+ hooks.build # build hooks
6
+ hooks.run_hooks(&block)
7
+ end
8
+ end
9
+ end
@@ -1,4 +1,4 @@
1
- module Terraspace::Terraform::Hooks
1
+ module Terraspace::Hooks
2
2
  module Dsl
3
3
  def before(*commands, **props)
4
4
  commands.each do |name|
@@ -13,7 +13,8 @@ module Terraspace::Terraform::Hooks
13
13
  end
14
14
 
15
15
  def each_hook(type, name, props={})
16
- @hooks[type][name] = props
16
+ @hooks[type][name] ||= []
17
+ @hooks[type][name] << props
17
18
  end
18
19
  end
19
20
  end
@@ -0,0 +1,23 @@
1
+ module Terraspace::Hooks
2
+ class Runner
3
+ include Terraspace::Util
4
+
5
+ def initialize(mod, hook)
6
+ @mod, @hook = mod, hook
7
+ @execute = @hook["execute"]
8
+ end
9
+
10
+ def run
11
+ case @execute
12
+ when String
13
+ Terraspace::Shell.new(@mod, @execute, exit_on_fail: @hook["exit_on_fail"]).run
14
+ when -> (e) { e.respond_to?(:public_instance_methods) && e.public_instance_methods.include?(:call) }
15
+ @execute.new.call
16
+ when -> (e) { e.respond_to?(:call) }
17
+ @execute.call
18
+ else
19
+ logger.warn "WARN: execute option not set for hook: #{@hook.inspect}"
20
+ end
21
+ end
22
+ end
23
+ end
@@ -34,8 +34,17 @@ module Terraspace
34
34
  Terraspace.check_project!
35
35
  return if root
36
36
 
37
- pretty_paths = paths.map { |p| Terraspace::Util.pretty_path(p) }
38
- logger.error "ERROR: Unable to find #{@name.color(:green)} module. Searched paths: #{pretty_paths}"
37
+ pretty_paths = paths.map { |p| Terraspace::Util.pretty_path(p) }.join(", ")
38
+ logger.error <<~EOL
39
+ ERROR: Unable to find #{@name.color(:green)}. Searched paths:
40
+
41
+ #{pretty_paths}
42
+
43
+ To see available stacks, try running:
44
+
45
+ terraspace list
46
+
47
+ EOL
39
48
  ENV['TS_TEST'] ? raise : exit(1)
40
49
  end
41
50
 
@@ -64,7 +64,9 @@ module Terraspace::Plugin::Summary
64
64
 
65
65
  def show_each(path)
66
66
  data = JSON.load(IO.read(path))
67
+ return unless data # edge case: blank file
67
68
  resources = data['resources']
69
+ return unless resources
68
70
  remove_statefile(path) if resources && resources.size == 0 && !ENV['TS_KEEP_EMPTY_STATEFILES']
69
71
  return unless resources && resources.size > 0
70
72
 
@@ -75,7 +77,7 @@ module Terraspace::Plugin::Summary
75
77
  identifier = r['instances'].map do |i|
76
78
  i['attributes']['name'] || i['attributes']['id']
77
79
  end.join(',')
78
- return if @options[:short]
80
+ return unless @options[:details]
79
81
  logger.info " #{r['type']} #{r['name']}: #{identifier}"
80
82
  end
81
83
  end
@@ -6,7 +6,8 @@ module Terraspace
6
6
 
7
7
  def initialize(mod, command, options={})
8
8
  @mod, @command, @options = mod, command, options
9
- @error_type, @error_messages = nil, ''
9
+ # error_messages holds aggregation of all error lines
10
+ @known_error, @error_messages = nil, ''
10
11
  end
11
12
 
12
13
  # requires @mod to be set
@@ -32,10 +33,9 @@ module Terraspace
32
33
  Open3.popen3(env, @command, chdir: @mod.cache_dir) do |stdin, stdout, stderr, wait_thread|
33
34
  mimic_terraform_input(stdin, stdout)
34
35
  while err = stderr.gets
35
- @error_type ||= known_error_type(err)
36
- if @error_type
37
- @error_messages << err
38
- else
36
+ @error_messages << err # aggregate all error lines
37
+ @known_error ||= known_error_type(err)
38
+ unless @known_error
39
39
  # Sometimes may print a "\e[31m\n" which like during dependencies fetcher init
40
40
  # suppress it so dont get a bunch of annoying "newlines"
41
41
  next if err == "\e[31m\n" && @options[:suppress_error_color]
@@ -49,8 +49,8 @@ module Terraspace
49
49
  end
50
50
 
51
51
  def known_error_type(err)
52
- if reinitialization_required?(err)
53
- :reinitialization_required
52
+ if reinit_required?(err)
53
+ :reinit_required
54
54
  elsif bucket_not_found?(err)
55
55
  :bucket_not_found
56
56
  end
@@ -61,7 +61,12 @@ module Terraspace
61
61
  err.include?("Failed to get existing workspaces")
62
62
  end
63
63
 
64
- def reinitialization_required?(err)
64
+ def reinit_required?(err)
65
+ # Example error: https://gist.github.com/tongueroo/f7e0a44b64f0a2e533089b18f331c21e
66
+ squeezed = @error_messages.gsub("\n", ' ').squeeze(' ') # remove double whitespaces and newlines
67
+ general_check = squeezed.include?("terraform init") && squeezed.include?("Error:")
68
+
69
+ general_check ||
65
70
  err.include?("reinitialization required") ||
66
71
  err.include?("terraform init") ||
67
72
  err.include?("require reinitialization")
@@ -71,9 +76,9 @@ module Terraspace
71
76
  return if status == 0
72
77
 
73
78
  exit_on_fail = @options[:exit_on_fail].nil? ? true : @options[:exit_on_fail]
74
- if @error_type == :reinitialization_required
79
+ if @known_error == :reinit_required
75
80
  raise InitRequiredError.new(@error_messages)
76
- elsif @error_type == :bucket_not_found
81
+ elsif @known_error == :bucket_not_found
77
82
  raise BucketNotFoundError.new(@error_messages)
78
83
  elsif exit_on_fail
79
84
  logger.error "Error running command: #{@command}".color(:red)
@@ -7,7 +7,7 @@ module Terraspace::Terraform::Args
7
7
  attr_accessor :name
8
8
  def initialize(mod, name)
9
9
  @mod, @name = mod, name
10
- @file = "#{Terraspace.root}/config/cli/args.rb"
10
+ @file = "#{Terraspace.root}/config/args/terraform.rb"
11
11
  @commands = {}
12
12
  end
13
13
 
@@ -11,18 +11,18 @@ module Terraspace::Terraform::RemoteState
11
11
  # Should always return a String
12
12
  def to_s
13
13
  if @mod.resolved
14
- # Dont use NullObject wrapper because Integer get changed to Strings.
14
+ # Dont use Unresolved wrapper because Integer get changed to Strings.
15
15
  # Want raw value to be used for the to_json call
16
16
  value = @raw.nil? ? mock_or_error : @raw
17
17
  value.to_json
18
18
  else
19
- NullObject.new # to_s => (unresolved)
19
+ Unresolved.new
20
20
  end
21
21
  end
22
22
 
23
23
  def to_ruby
24
24
  data = @raw.nil? ? mock_or_error : @raw
25
- @mod.resolved ? data : NullObject.new
25
+ @mod.resolved ? data : Unresolved.new
26
26
  end
27
27
 
28
28
  private
@@ -1,5 +1,5 @@
1
1
  module Terraspace::Terraform::RemoteState
2
- class NullObject
2
+ class Unresolved
3
3
  def to_a
4
4
  []
5
5
  end
@@ -1,6 +1,7 @@
1
1
  module Terraspace::Terraform
2
2
  class Runner < Terraspace::CLI::Base
3
3
  extend Memoist
4
+ include Terraspace::Hooks::Concern
4
5
  include Terraspace::Util
5
6
 
6
7
  attr_reader :name
@@ -21,7 +22,7 @@ module Terraspace::Terraform
21
22
 
22
23
  params = args.flatten.join(' ')
23
24
  command = "terraform #{name} #{params}"
24
- run_hooks(name) do
25
+ run_hooks("terraform.rb", name) do
25
26
  Terraspace::Shell.new(@mod, command, @options.merge(env: custom.env_vars)).run
26
27
  end
27
28
  rescue Terraspace::InitRequiredError => e
@@ -53,12 +54,6 @@ module Terraspace::Terraform
53
54
  @options[:quiet] ? logger.debug(msg) : logger.info(msg)
54
55
  end
55
56
 
56
- def run_hooks(name, &block)
57
- hooks = Hooks::Builder.new(@mod, name)
58
- hooks.build # build hooks
59
- hooks.run_hooks(&block)
60
- end
61
-
62
57
  def args
63
58
  # base at end in case of redirection. IE: terraform output > /path
64
59
  custom.args + custom.var_files + default.args