terraspace 0.5.12 → 0.6.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +30 -7
  3. data/README.md +2 -0
  4. data/lib/terraspace/app.rb +35 -15
  5. data/lib/terraspace/app/inits.rb +13 -0
  6. data/lib/terraspace/booter.rb +19 -2
  7. data/lib/terraspace/cli.rb +18 -5
  8. data/lib/terraspace/cli/commander.rb +1 -1
  9. data/lib/terraspace/cli/concerns/source_dirs.rb +13 -0
  10. data/lib/terraspace/cli/down.rb +1 -1
  11. data/lib/terraspace/cli/fmt.rb +21 -0
  12. data/lib/terraspace/cli/fmt/runner.rb +64 -0
  13. data/lib/terraspace/cli/help/fmt.md +10 -0
  14. data/lib/terraspace/cli/help/{cloud → tfc}/destroy.md +1 -1
  15. data/lib/terraspace/cli/help/{cloud → tfc}/list.md +1 -1
  16. data/lib/terraspace/cli/help/{cloud → tfc}/runs/list.md +3 -3
  17. data/lib/terraspace/cli/help/{cloud → tfc}/runs/prune.md +3 -3
  18. data/lib/terraspace/cli/help/{cloud → tfc}/sync.md +3 -3
  19. data/lib/terraspace/cli/init.rb +21 -8
  20. data/lib/terraspace/cli/list.rb +3 -2
  21. data/lib/terraspace/cli/new/helper.rb +2 -2
  22. data/lib/terraspace/cli/state.rb +10 -0
  23. data/lib/terraspace/cli/{cloud.rb → tfc.rb} +3 -3
  24. data/lib/terraspace/cli/{cloud → tfc}/runs.rb +4 -4
  25. data/lib/terraspace/compiler/builder.rb +4 -1
  26. data/lib/terraspace/compiler/helper_extender.rb +3 -3
  27. data/lib/terraspace/compiler/strategy/tfvar.rb +16 -4
  28. data/lib/terraspace/compiler/strategy/tfvar/layer.rb +75 -52
  29. data/lib/terraspace/ext.rb +1 -0
  30. data/lib/terraspace/ext/core/string.rb +5 -0
  31. data/lib/terraspace/layering.rb +24 -0
  32. data/lib/terraspace/logger.rb +8 -1
  33. data/lib/terraspace/mod.rb +18 -3
  34. data/lib/terraspace/plugin/expander/friendly.rb +10 -0
  35. data/lib/terraspace/plugin/expander/interface.rb +6 -1
  36. data/lib/terraspace/plugin/summary/interface.rb +1 -1
  37. data/lib/terraspace/shell.rb +16 -1
  38. data/lib/terraspace/terraform/api/runs.rb +13 -2
  39. data/lib/terraspace/terraform/api/token.rb +2 -2
  40. data/lib/terraspace/terraform/api/var.rb +1 -1
  41. data/lib/terraspace/terraform/api/vars.rb +1 -1
  42. data/lib/terraspace/terraform/api/vars/base.rb +2 -0
  43. data/lib/terraspace/terraform/api/vars/json.rb +13 -1
  44. data/lib/terraspace/terraform/api/workspace.rb +10 -3
  45. data/lib/terraspace/terraform/args/default.rb +23 -13
  46. data/lib/terraspace/terraform/ihooks/after/plan.rb +17 -0
  47. data/lib/terraspace/terraform/ihooks/base.rb +8 -0
  48. data/lib/terraspace/terraform/ihooks/before/plan.rb +14 -0
  49. data/lib/terraspace/terraform/remote_state/fetcher.rb +1 -1
  50. data/lib/terraspace/terraform/runner.rb +12 -0
  51. data/lib/terraspace/terraform/{cloud → tfc}/runs.rb +1 -1
  52. data/lib/terraspace/terraform/{cloud → tfc}/runs/base.rb +1 -1
  53. data/lib/terraspace/terraform/{cloud → tfc}/runs/item_presenter.rb +1 -1
  54. data/lib/terraspace/terraform/{cloud → tfc}/runs/lister.rb +1 -1
  55. data/lib/terraspace/terraform/{cloud → tfc}/runs/pruner.rb +1 -1
  56. data/lib/terraspace/terraform/{cloud → tfc}/sync.rb +2 -2
  57. data/lib/terraspace/terraform/{cloud → tfc}/syncer.rb +1 -1
  58. data/lib/terraspace/terraform/{cloud → tfc}/workspace.rb +2 -3
  59. data/lib/terraspace/util/pretty.rb +2 -1
  60. data/lib/terraspace/version.rb +1 -1
  61. metadata +29 -18
  62. data/lib/terraspace/app/hooks.rb +0 -18
@@ -1,2 +1,3 @@
1
1
  require_relative "ext/bundler"
2
2
  require_relative "ext/core/module"
3
+ require_relative "ext/core/string"
@@ -0,0 +1,5 @@
1
+ class String
2
+ def camelcase
3
+ self.underscore.camelize
4
+ end
5
+ end
@@ -0,0 +1,24 @@
1
+ require "active_support/lazy_load_hooks"
2
+
3
+ module Terraspace
4
+ module Layering
5
+ def layers
6
+ pre_layers + main_layers + post_layers
7
+ end
8
+
9
+ def main_layers
10
+ # '' prefix for base layer
11
+ [''] + plugins
12
+ end
13
+
14
+ def pre_layers
15
+ []
16
+ end
17
+
18
+ def post_layers
19
+ []
20
+ end
21
+ end
22
+ end
23
+
24
+ ActiveSupport.run_load_hooks(:terraspace_layering, Terraspace::Layering)
@@ -5,7 +5,7 @@ module Terraspace
5
5
  def initialize(*args)
6
6
  super
7
7
  self.formatter = Formatter.new
8
- self.level = :info
8
+ self.level = ENV['TS_LOG_LEVEL'] || :info # note: only respected when config.logger not set in config/app.rb
9
9
  end
10
10
 
11
11
  def format_message(severity, datetime, progname, msg)
@@ -16,5 +16,12 @@ module Terraspace
16
16
  end
17
17
  line =~ /\n$/ ? line : "#{line}\n"
18
18
  end
19
+
20
+ # Used to allow terraform output to always go to stdout
21
+ # Terraspace output goes to stderr by default
22
+ # See: terraspace/shell.rb
23
+ def stdout(msg)
24
+ puts msg
25
+ end
19
26
  end
20
27
  end
@@ -75,7 +75,7 @@ module Terraspace
75
75
  #
76
76
  # down - so user can delete stacks w/o needing to create an empty app/stacks/demo folder
77
77
  # null - for the terraspace summary command when there are zero stacks.
78
- # Also useful for terraspace cloud list_workspaces
78
+ # Also useful for terraspace tfc list_workspaces
79
79
  #
80
80
  def possible_fake_root
81
81
  if @options[:command] == "down"
@@ -111,9 +111,24 @@ module Terraspace
111
111
 
112
112
  # Full path with build_dir
113
113
  def cache_dir
114
- pattern = Terraspace.config.build.cache_dir # IE: :CACHE_ROOT/:REGION/:ENV/:BUILD_DIR
114
+ # config.build.cache_dir is a String or object that respond_to call. IE:
115
+ # :CACHE_ROOT/:REGION/:ENV/:BUILD_DIR
116
+ # CustomBuildDir.call
117
+ # The call method should return a String pattern used for substitutions
118
+ object = Terraspace.config.build.cache_dir
119
+ pattern = if object.is_a?(String)
120
+ object
121
+ elsif object.respond_to?(:call)
122
+ object.call(self)
123
+ elsif object.public_instance_methods.include?(:call)
124
+ instance = object.new
125
+ instance.call(self)
126
+ else
127
+ raise "ERROR: config.build.cache_dir is not a String or responds to the .call method."
128
+ end
129
+
115
130
  expander = Terraspace::Compiler::Expander.autodetect(self)
116
- expander.expansion(pattern)
131
+ expander.expansion(pattern) # pattern is a String that contains placeholders for substitutions
117
132
  end
118
133
  memoize :cache_dir
119
134
 
@@ -0,0 +1,10 @@
1
+ module Terraspace::Plugin::Expander
2
+ module Friendly
3
+ # used by
4
+ # Terraspace::Compiler::Strategy::Tfvar::Layer
5
+ # Terraspace::Plugin::Expander::Interface
6
+ def friendly_name(name)
7
+ Terraspace.config.layering.names[name.to_sym] || name
8
+ end
9
+ end
10
+ end
@@ -7,6 +7,7 @@
7
7
  module Terraspace::Plugin::Expander
8
8
  module Interface
9
9
  include Terraspace::Plugin::InferProvider
10
+ include Terraspace::Plugin::Expander::Friendly
10
11
 
11
12
  delegate :build_dir, :type_dir, :type, to: :mod
12
13
 
@@ -68,7 +69,11 @@ module Terraspace::Plugin::Expander
68
69
 
69
70
  def var_value(name)
70
71
  name = name.sub(':','').downcase
71
- send(name)
72
+ value = send(name)
73
+ if name == "namespace" && Terraspace.config.layering.enable_names.expansion
74
+ value = friendly_name(value)
75
+ end
76
+ value
72
77
  end
73
78
 
74
79
  def mod_name
@@ -67,7 +67,7 @@ module Terraspace::Plugin::Summary
67
67
  return unless data # edge case: blank file
68
68
  resources = data['resources']
69
69
  return unless resources
70
- remove_statefile(path) if resources && resources.size == 0 && !ENV['TS_KEEP_EMPTY_STATEFILES']
70
+ remove_statefile(path) if Terraspace.config.summary.prune && resources && resources.size == 0
71
71
  return unless resources && resources.size > 0
72
72
 
73
73
  pretty_path = path.sub(Regexp.new(".*#{@bucket}/#{@folder}"), '')
@@ -30,6 +30,10 @@ module Terraspace
30
30
  def popen3(env)
31
31
  Open3.popen3(env, @command, chdir: @mod.cache_dir) do |stdin, stdout, stderr, wait_thread|
32
32
  mimic_terraform_input(stdin, stdout)
33
+ while out = stdout.gets
34
+ terraform_to_stdout(out)
35
+ end
36
+
33
37
  while err = stderr.gets
34
38
  @error ||= Error.new
35
39
  @error.lines << err # aggregate all error lines
@@ -69,7 +73,7 @@ module Terraspace
69
73
  "\e[0m\e[1mvar.", # prompts for variable input. can happen on plan or apply. looking for bold marker also in case "var." shows up somewhere else
70
74
  ]
71
75
  while out = stdout.gets
72
- logger.info(out) unless shown && out.include?("Enter a value:")
76
+ terraform_to_stdout(out) unless shown && out.include?("Enter a value:")
73
77
  shown = false if out.include?("Enter a value:") # reset shown in case of multiple input prompts
74
78
 
75
79
  # Sometimes stdout doesnt flush and show "Enter a value: ", so mimic it
@@ -80,5 +84,16 @@ module Terraspace
80
84
  end
81
85
  end
82
86
  end
87
+
88
+ # Allows piping to jq. IE:
89
+ # terraspace show demo --json | jq
90
+ def terraform_to_stdout(out)
91
+ # so terraform output goes stdout
92
+ if logger.respond_to?(:stdout) && !@options[:log_to_stderr]
93
+ logger.stdout(out)
94
+ else
95
+ logger.info(out)
96
+ end
97
+ end
83
98
  end
84
99
  end
@@ -8,8 +8,19 @@ class Terraspace::Terraform::Api
8
8
  end
9
9
 
10
10
  def list
11
- payload = http.get("workspaces/#{@workspace_id}/runs")
12
- payload['data'] if payload
11
+ data, next_page = [], :start
12
+ while next_page == :start || next_page
13
+ url = "workspaces/#{@workspace_id}/runs"
14
+ if next_page
15
+ qs = URI.encode_www_form('page[number]': next_page) if next_page
16
+ url += "?#{qs}"
17
+ end
18
+ payload = http.get(url)
19
+ return unless payload
20
+ data += payload['data']
21
+ next_page = payload['meta']['pagination']['next-page']
22
+ end
23
+ data
13
24
  end
14
25
 
15
26
  def discard(id)
@@ -51,11 +51,11 @@ class Terraspace::Terraform::Api
51
51
  end
52
52
 
53
53
  def hostname
54
- ENV['TS_HOST'] || Terraspace.config.cloud.hostname || 'app.terraform.io'
54
+ ENV['TFC_HOST'] || Terraspace.config.tfc.hostname || 'app.terraform.io'
55
55
  end
56
56
 
57
57
  def hostname_configured?
58
- !!Terraspace.config.cloud.hostname
58
+ !!Terraspace.config.tfc.hostname
59
59
  end
60
60
 
61
61
  def self.get
@@ -31,7 +31,7 @@ class Terraspace::Terraform::Api
31
31
  end
32
32
 
33
33
  def vars
34
- Terraspace.config.cloud.vars
34
+ Terraspace.config.tfc.vars
35
35
  end
36
36
 
37
37
  def variable_id(key)
@@ -33,7 +33,7 @@ class Terraspace::Terraform::Api
33
33
 
34
34
  def vars_path
35
35
  # .rb takes higher precedence
36
- Dir.glob("#{Terraspace.root}/config/terraform/cloud/vars.{rb,json}").first
36
+ Dir.glob("#{Terraspace.root}/config/terraform/tfc/vars.{rb,json}").first
37
37
  end
38
38
  end
39
39
  end
@@ -1,5 +1,7 @@
1
1
  class Terraspace::Terraform::Api::Vars
2
2
  class Base
3
+ include Terraspace::Util::Logging
4
+
3
5
  def initialize(mod, vars_path)
4
6
  @mod, @vars_path = mod, vars_path
5
7
  end
@@ -4,11 +4,23 @@ class Terraspace::Terraform::Api::Vars
4
4
  context = Terraspace::Compiler::Erb::Context.new(@mod)
5
5
  result = RenderMePretty.result(@vars_path, context: context)
6
6
 
7
- data = JSON.load(result)
7
+ data = json_load(result)
8
8
  items = data.select do |item|
9
9
  item['data']['type'] == 'vars'
10
10
  end
11
11
  items.map { |i| i['data']['attributes'] }
12
12
  end
13
+
14
+ def json_load(result)
15
+ JSON.load(result)
16
+ rescue JSON::ParserError => e
17
+ # TODO: show exact line with error
18
+ logger.info("ERROR in json: #{e.class}: #{e.message}")
19
+ path = "/tmp/terraspace/debug/vars.json"
20
+ logger.info("Result also written to #{path} for inspection")
21
+ FileUtils.mkdir_p(File.dirname(path))
22
+ IO.write(path, result)
23
+ exit 1
24
+ end
13
25
  end
14
26
  end
@@ -24,7 +24,7 @@ class Terraspace::Terraform::Api
24
24
 
25
25
  def working_directory
26
26
  cache_dir = @mod.cache_dir.sub("#{Terraspace.root}/", '')
27
- prefix = Terraspace.config.cloud.working_dir_prefix # prepended to TFC Working Directory
27
+ prefix = Terraspace.config.tfc.working_dir_prefix # prepended to TFC Working Directory
28
28
  prefix ? "#{prefix}/#{cache_dir}" : cache_dir
29
29
  end
30
30
 
@@ -38,7 +38,8 @@ class Terraspace::Terraform::Api
38
38
  #
39
39
  # terraspace up demo --no-init
40
40
  #
41
- unless payload || options[:exit_on_fail] == false
41
+ exit_on_fail = options[:exit_on_fail].nil? ? true : options[:exit_on_fail]
42
+ if exit_on_fail && not_found_error?(payload)
42
43
  logger.error "ERROR: Unable to find the workspace: #{@name}. The workspace may not exist. Or the Terraform token may be invalid. Please double check your Terraform token.".color(:red)
43
44
  exit 1
44
45
  end
@@ -46,6 +47,12 @@ class Terraspace::Terraform::Api
46
47
  end
47
48
  memoize :details
48
49
 
50
+ def not_found_error?(payload)
51
+ return true unless payload
52
+ return false unless payload.key?('errors')
53
+ payload['errors'][0]['status'] == '404'
54
+ end
55
+
49
56
  def destroy
50
57
  # response payload from delete operation is nil
51
58
  http.delete("/organizations/#{@organization}/workspaces/#{@name}")
@@ -74,7 +81,7 @@ class Terraspace::Terraform::Api
74
81
 
75
82
  def attributes
76
83
  attrs = { name: @name }
77
- config = Terraspace.config.cloud.workspace.attrs
84
+ config = Terraspace.config.tfc.workspace.attrs
78
85
  attrs.merge!(config)
79
86
  # Default: run on all changes since app/modules can affect app/stacks
80
87
  if config['vcs-repo'] && config['file-triggers-enabled'].nil?
@@ -23,7 +23,10 @@ module Terraspace::Terraform::Args
23
23
  args = auto_approve_arg
24
24
  var_files = @options[:var_files]
25
25
  if var_files
26
- args << var_files.map { |f| "-var-file #{Dir.pwd}/#{f}" }.join(' ')
26
+ var_files.each do |file|
27
+ copy_to_cache(plan)
28
+ end
29
+ args << var_files.map { |f| "-var-file #{f}" }.join(' ')
27
30
  end
28
31
 
29
32
  args << input_option
@@ -31,15 +34,8 @@ module Terraspace::Terraform::Args
31
34
  # must be at the end
32
35
  plan = @options[:plan]
33
36
  if plan
34
- if plan.starts_with?('/')
35
- src = plan
36
- dest = src
37
- else
38
- src = "#{Dir.pwd}/#{plan}"
39
- dest = "#{@mod.cache_dir}/#{File.basename(src)}"
40
- end
41
- FileUtils.cp(src, dest) unless same_file?(src, dest)
42
- args << " #{dest}"
37
+ copy_to_cache(plan)
38
+ args << " #{plan}"
43
39
  end
44
40
  args
45
41
  end
@@ -79,19 +75,24 @@ module Terraspace::Terraform::Args
79
75
  args << input_option
80
76
  args << "-destroy" if @options[:destroy]
81
77
  args << "-out #{expanded_out}" if @options[:out]
78
+ # Note: based on the @options[:out] will run an internal hook to copy plan
79
+ # file back up to the root project folder for use. Think this is convenient and expected behavior.
82
80
  args
83
81
  end
84
82
 
85
83
  def show_args
86
84
  args = []
87
85
  args << " -json" if @options[:json]
88
- args << " #{@options[:plan]}" if @options[:plan] # terraform show /path/to/plan
86
+ plan = @options[:plan]
87
+ if plan
88
+ copy_to_cache(@options[:plan])
89
+ args << " #{@options[:plan]}" # terraform show /path/to/plan
90
+ end
89
91
  args
90
92
  end
91
93
 
92
94
  def expanded_out
93
- out = @options[:out]
94
- out.starts_with?('/') ? out : "#{Dir.pwd}/#{out}"
95
+ @options[:out]
95
96
  end
96
97
 
97
98
  def destroy_args
@@ -123,5 +124,14 @@ module Terraspace::Terraform::Args
123
124
  def same_file?(src, dest)
124
125
  src == dest
125
126
  end
127
+
128
+ def copy_to_cache(file)
129
+ return if file =~ %r{^/} # not need to copy absolute path
130
+ name = file.sub("#{Terraspace.root}/",'')
131
+ src = name
132
+ dest = "#{@mod.cache_dir}/#{name}"
133
+ FileUtils.mkdir_p(File.dirname(dest))
134
+ FileUtils.cp(src, dest) unless same_file?(src, dest)
135
+ end
126
136
  end
127
137
  end
@@ -0,0 +1,17 @@
1
+ module Terraspace::Terraform::Ihooks::After
2
+ class Plan < Terraspace::Terraform::Ihooks::Base
3
+ def run
4
+ return if !@options[:out] || @options[:copy_to_root] == false
5
+ copy_to_root(@options[:out])
6
+ end
7
+
8
+ def copy_to_root(file)
9
+ return if file =~ %r{^/} # not need to copy absolute path
10
+ name = file.sub("#{Terraspace.root}/",'')
11
+ src = "#{@mod.cache_dir}/#{name}"
12
+ dest = name
13
+ FileUtils.mkdir_p(File.dirname(dest))
14
+ FileUtils.cp(src, dest)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,8 @@
1
+ module Terraspace::Terraform::Ihooks
2
+ class Base < Terraspace::CLI::Base
3
+ def initialize(name, options={})
4
+ @name = name
5
+ super(options)
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,14 @@
1
+ module Terraspace::Terraform::Ihooks::Before
2
+ class Plan < Terraspace::Terraform::Ihooks::Base
3
+ def run
4
+ out = @options[:out]
5
+ return unless out
6
+ return if out =~ %r{^/} # not need to create parent dir for copy with absolute path
7
+
8
+ out = @options[:out]
9
+ name = out.sub("#{Terraspace.root}/",'')
10
+ dest = "#{@mod.cache_dir}/#{name}"
11
+ FileUtils.mkdir_p(File.dirname(dest))
12
+ end
13
+ end
14
+ end
@@ -84,7 +84,7 @@ module Terraspace::Terraform::RemoteState
84
84
  end
85
85
 
86
86
  def init
87
- Terraspace::CLI::Init.new(mod: @child.name, calling_command: "apply", quiet: true, suppress_error_color: true).init
87
+ Terraspace::CLI::Init.new(mod: @child.name, quiet: true, suppress_error_color: true).init
88
88
  true
89
89
  rescue Terraspace::BucketNotFoundError # from Terraspace::Shell
90
90
  bucket_not_found_error
@@ -22,7 +22,9 @@ module Terraspace::Terraform
22
22
  params = args.flatten.join(' ')
23
23
  command = "terraform #{name} #{params}".squish
24
24
  run_hooks("terraform.rb", name) do
25
+ run_internal_hook(:before, name)
25
26
  Terraspace::Shell.new(@mod, command, @options.merge(env: custom.env_vars)).run
27
+ run_internal_hook(:after, name)
26
28
  end
27
29
  rescue Terraspace::SharedCacheError, Terraspace::InitRequiredError
28
30
  @retryer ||= Retryer.new(@mod, @options, name, $!)
@@ -34,6 +36,16 @@ module Terraspace::Terraform
34
36
  end
35
37
  end
36
38
 
39
+ def run_internal_hook(type, name)
40
+ begin
41
+ klass = "Terraspace::Terraform::Ihooks::#{type.to_s.classify}::#{name.classify}".constantize
42
+ rescue NameError
43
+ return
44
+ end
45
+ ihook = klass.new(name, @options)
46
+ ihook.run
47
+ end
48
+
37
49
  @@current_dir_message_shown = false
38
50
  def current_dir_message
39
51
  return if @@current_dir_message_shown