terraspace 0.5.9 → 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE.md +2 -2
  3. data/.github/ISSUE_TEMPLATE/bug_report.md +4 -3
  4. data/.github/ISSUE_TEMPLATE/question.md +2 -2
  5. data/CHANGELOG.md +29 -7
  6. data/README.md +2 -0
  7. data/lib/terraspace.rb +1 -0
  8. data/lib/terraspace/app.rb +36 -16
  9. data/lib/terraspace/app/inits.rb +13 -0
  10. data/lib/terraspace/booter.rb +1 -0
  11. data/lib/terraspace/cli.rb +13 -6
  12. data/lib/terraspace/cli/build/placeholder.rb +6 -1
  13. data/lib/terraspace/cli/commander.rb +1 -1
  14. data/lib/terraspace/cli/down.rb +1 -1
  15. data/lib/terraspace/cli/help/{cloud → tfc}/destroy.md +1 -1
  16. data/lib/terraspace/cli/help/{cloud → tfc}/list.md +1 -1
  17. data/lib/terraspace/cli/help/{cloud → tfc}/runs/list.md +3 -3
  18. data/lib/terraspace/cli/help/{cloud → tfc}/runs/prune.md +3 -3
  19. data/lib/terraspace/cli/help/{cloud → tfc}/sync.md +3 -3
  20. data/lib/terraspace/cli/init.rb +21 -8
  21. data/lib/terraspace/cli/state.rb +10 -0
  22. data/lib/terraspace/cli/{cloud.rb → tfc.rb} +3 -3
  23. data/lib/terraspace/cli/{cloud → tfc}/runs.rb +4 -4
  24. data/lib/terraspace/cli/up.rb +5 -3
  25. data/lib/terraspace/compiler/builder.rb +2 -0
  26. data/lib/terraspace/compiler/dirs_concern.rb +11 -1
  27. data/lib/terraspace/compiler/select.rb +28 -0
  28. data/lib/terraspace/compiler/strategy/tfvar.rb +16 -4
  29. data/lib/terraspace/compiler/strategy/tfvar/layer.rb +75 -52
  30. data/lib/terraspace/layering.rb +24 -0
  31. data/lib/terraspace/logger.rb +8 -1
  32. data/lib/terraspace/mod.rb +18 -3
  33. data/lib/terraspace/plugin/expander/friendly.rb +10 -0
  34. data/lib/terraspace/plugin/expander/interface.rb +6 -1
  35. data/lib/terraspace/plugin/summary/interface.rb +1 -1
  36. data/lib/terraspace/shell.rb +16 -1
  37. data/lib/terraspace/shell/error.rb +1 -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 +26 -18
@@ -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
@@ -13,7 +13,7 @@ class Terraspace::Shell
13
13
  if reinit_required?
14
14
  Terraspace::InitRequiredError.new(@lines)
15
15
  elsif bucket_not_found?
16
- Terraspace::BucketNotFound.new(@lines)
16
+ Terraspace::BucketNotFoundError.new(@lines)
17
17
  elsif shared_cache_error?
18
18
  Terraspace::SharedCacheError.new(@lines)
19
19
  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
@@ -1,4 +1,4 @@
1
- module Terraspace::Terraform::Cloud
1
+ module Terraspace::Terraform::Tfc
2
2
  class Runs < Terraspace::CLI::Base
3
3
  def list
4
4
  lister = Lister.new(@mod, @options)
@@ -1,4 +1,4 @@
1
- class Terraspace::Terraform::Cloud::Runs
1
+ class Terraspace::Terraform::Tfc::Runs
2
2
  class Base
3
3
  extend Memoist
4
4
  include Terraspace::Util::Logging
@@ -1,4 +1,4 @@
1
- class Terraspace::Terraform::Cloud::Runs
1
+ class Terraspace::Terraform::Tfc::Runs
2
2
  class ItemPresenter
3
3
  attr_reader :id
4
4
  def initialize(raw)
@@ -1,4 +1,4 @@
1
- class Terraspace::Terraform::Cloud::Runs
1
+ class Terraspace::Terraform::Tfc::Runs
2
2
  class Lister < Base
3
3
  def run
4
4
  build_project
@@ -1,4 +1,4 @@
1
- class Terraspace::Terraform::Cloud::Runs
1
+ class Terraspace::Terraform::Tfc::Runs
2
2
  class Pruner < Base
3
3
  include Terraspace::Terraform::Api::Client
4
4
 
@@ -1,4 +1,4 @@
1
- module Terraspace::Terraform::Cloud
1
+ module Terraspace::Terraform::Tfc
2
2
  class Sync < Terraspace::CLI::Base
3
3
  extend Memoist
4
4
  include Terraspace::Terraform::Api::Client
@@ -16,7 +16,7 @@ module Terraspace::Terraform::Cloud
16
16
  # So we check and create the workspace if necessary.
17
17
  def run
18
18
  # Note: workspace still gets created by `terraform init` However, variables wont be sync if returns early
19
- return unless Terraspace.config.cloud.auto_sync || @options[:override_auto_sync]
19
+ return unless Terraspace.config.tfc.auto_sync || @options[:override_auto_sync]
20
20
  return unless workspaces_backend?
21
21
  logger.info "Syncing to Terraform Cloud: #{@mod.name} => #{workspace_name}"
22
22
  @api = Terraspace::Terraform::Api.new(@mod, remote)
@@ -1,4 +1,4 @@
1
- module Terraspace::Terraform::Cloud
1
+ module Terraspace::Terraform::Tfc
2
2
  class Syncer < Terraspace::CLI::Base
3
3
  extend Memoist
4
4
  include Terraspace::Compiler::DirsConcern
@@ -1,4 +1,4 @@
1
- module Terraspace::Terraform::Cloud
1
+ module Terraspace::Terraform::Tfc
2
2
  class Workspace < Terraspace::CLI::Base
3
3
  extend Memoist
4
4
  include Terraspace::Util::Logging
@@ -21,7 +21,7 @@ module Terraspace::Terraform::Cloud
21
21
  end
22
22
 
23
23
  def init
24
- Terraspace::CLI::Init.new(@options.merge(calling_command: "cloud-setup")).run
24
+ Terraspace::CLI::Init.new(@options).run
25
25
  end
26
26
 
27
27
  def create
@@ -72,4 +72,3 @@ module Terraspace::Terraform::Cloud
72
72
  end
73
73
  end
74
74
  end
75
-