terraspace 0.3.6 → 0.4.0

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 (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