terraspace 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (104) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +33 -3
  3. data/README.md +34 -13
  4. data/lib/templates/base/git_hook/hook.sh +1 -1
  5. data/lib/templates/base/project/.gitignore +1 -0
  6. data/lib/templates/base/project/README.md +17 -0
  7. data/lib/terraspace.rb +4 -0
  8. data/lib/terraspace/all/base.rb +8 -0
  9. data/lib/terraspace/all/grapher.rb +129 -0
  10. data/lib/terraspace/all/preview.rb +43 -0
  11. data/lib/terraspace/all/runner.rb +169 -0
  12. data/lib/terraspace/all/summary.rb +99 -0
  13. data/lib/terraspace/app.rb +31 -8
  14. data/lib/terraspace/booter.rb +9 -0
  15. data/lib/terraspace/builder.rb +53 -20
  16. data/lib/terraspace/cli.rb +39 -12
  17. data/lib/terraspace/cli/all.rb +63 -0
  18. data/lib/terraspace/cli/build/placeholder.rb +2 -5
  19. data/lib/terraspace/cli/bundle.rb +1 -1
  20. data/lib/terraspace/cli/check_setup.rb +17 -5
  21. data/lib/terraspace/cli/cloud.rb +19 -3
  22. data/lib/terraspace/cli/cloud/runs.rb +24 -0
  23. data/lib/terraspace/cli/commander.rb +6 -3
  24. data/lib/terraspace/cli/down.rb +20 -0
  25. data/lib/terraspace/cli/help/cloud/runs/list.md +36 -0
  26. data/lib/terraspace/cli/help/cloud/runs/prune.md +25 -0
  27. data/lib/terraspace/cli/help/cloud/sync.md +19 -0
  28. data/lib/terraspace/cli/help/log.md +42 -0
  29. data/lib/terraspace/cli/init.rb +35 -7
  30. data/lib/terraspace/cli/list.rb +14 -1
  31. data/lib/terraspace/cli/log.rb +112 -0
  32. data/lib/terraspace/cli/log/concern.rb +24 -0
  33. data/lib/terraspace/cli/logs.rb +15 -0
  34. data/lib/terraspace/cli/logs/tasks.rb +32 -0
  35. data/lib/terraspace/cli/new/git_hook.rb +1 -1
  36. data/lib/terraspace/cli/summary.rb +1 -1
  37. data/lib/terraspace/cli/tfc_concern.rb +14 -0
  38. data/lib/terraspace/cli/up.rb +32 -0
  39. data/lib/terraspace/compiler/builder.rb +3 -3
  40. data/lib/terraspace/compiler/cleaner.rb +1 -1
  41. data/lib/terraspace/compiler/cleaner/backend_change.rb +21 -7
  42. data/lib/terraspace/compiler/dirs_concern.rb +47 -0
  43. data/lib/terraspace/compiler/dsl/syntax/helpers/common.rb +26 -1
  44. data/lib/terraspace/core.rb +11 -2
  45. data/lib/terraspace/dependency/graph.rb +139 -0
  46. data/lib/terraspace/dependency/node.rb +38 -0
  47. data/lib/terraspace/dependency/registry.rb +11 -0
  48. data/lib/terraspace/logger.rb +6 -18
  49. data/lib/terraspace/logger/formatter.rb +13 -0
  50. data/lib/terraspace/mod.rb +7 -1
  51. data/lib/terraspace/plugin/summary/interface.rb +1 -0
  52. data/lib/terraspace/seeder/where.rb +6 -2
  53. data/lib/terraspace/shell.rb +79 -0
  54. data/lib/terraspace/terraform/api.rb +7 -40
  55. data/lib/terraspace/terraform/api/base.rb +7 -0
  56. data/lib/terraspace/terraform/api/client.rb +23 -3
  57. data/lib/terraspace/terraform/api/http.rb +14 -34
  58. data/lib/terraspace/terraform/api/http/concern.rb +10 -0
  59. data/lib/terraspace/terraform/api/runs.rb +28 -0
  60. data/lib/terraspace/terraform/api/token.rb +65 -0
  61. data/lib/terraspace/terraform/api/var.rb +20 -6
  62. data/lib/terraspace/terraform/api/vars.rb +2 -1
  63. data/lib/terraspace/terraform/api/workspace.rb +98 -0
  64. data/lib/terraspace/terraform/args/default.rb +48 -21
  65. data/lib/terraspace/terraform/cloud/runs.rb +13 -0
  66. data/lib/terraspace/terraform/cloud/runs/base.rb +33 -0
  67. data/lib/terraspace/terraform/cloud/runs/item_presenter.rb +37 -0
  68. data/lib/terraspace/terraform/cloud/runs/lister.rb +22 -0
  69. data/lib/terraspace/terraform/cloud/runs/pruner.rb +109 -0
  70. data/lib/terraspace/terraform/cloud/sync.rb +41 -0
  71. data/lib/terraspace/terraform/cloud/syncer.rb +52 -0
  72. data/lib/terraspace/terraform/cloud/workspace.rb +10 -21
  73. data/lib/terraspace/terraform/hooks/builder.rb +1 -1
  74. data/lib/terraspace/terraform/remote_state/fetcher.rb +122 -0
  75. data/lib/terraspace/terraform/remote_state/marker/output.rb +39 -0
  76. data/lib/terraspace/terraform/remote_state/marker/pretty_tracer.rb +37 -0
  77. data/lib/terraspace/terraform/remote_state/output_proxy.rb +29 -0
  78. data/lib/terraspace/terraform/runner.rb +24 -14
  79. data/lib/terraspace/util.rb +1 -5
  80. data/lib/terraspace/util/pretty.rb +18 -0
  81. data/lib/terraspace/version.rb +1 -1
  82. data/spec/fixtures/fetcher/c1.json +37 -0
  83. data/spec/fixtures/parser/cache_dirs/all/01-test.auto.tfvars +5 -0
  84. data/spec/fixtures/parser/cache_dirs/depends_on/01-test.auto.tfvars +2 -0
  85. data/spec/fixtures/parser/cache_dirs/output/01-test.auto.tfvars +2 -0
  86. data/spec/fixtures/summary/down.log +12 -0
  87. data/spec/fixtures/summary/output.log +5 -0
  88. data/spec/fixtures/summary/plan/error.log +20 -0
  89. data/spec/fixtures/summary/plan/success.log +17 -0
  90. data/spec/fixtures/summary/show.log +22 -0
  91. data/spec/fixtures/summary/up/error.log +13 -0
  92. data/spec/fixtures/summary/up/success.log +63 -0
  93. data/spec/fixtures/summary/validate/error.log +13 -0
  94. data/spec/fixtures/summary/validate/success.log +5 -0
  95. data/spec/terraspace/all/grapher_spec.rb +38 -0
  96. data/spec/terraspace/all/runner_spec.rb +48 -0
  97. data/spec/terraspace/all/summary_spec.rb +93 -0
  98. data/spec/terraspace/dependency/graph_spec.rb +162 -0
  99. data/spec/terraspace/seeder_spec.rb +0 -1
  100. data/spec/terraspace/terraform/remote_state/fetcher_spec.rb +52 -0
  101. data/terraspace.gemspec +5 -1
  102. metadata +137 -5
  103. data/lib/terraspace/terraform/cloud.rb +0 -25
  104. data/lib/terraspace/util/sh.rb +0 -19
@@ -0,0 +1,22 @@
1
+ require 'cli-format'
2
+
3
+ class Terraspace::Terraform::Cloud::Runs
4
+ class Lister < Base
5
+ def run
6
+ build_project
7
+ if runs.empty?
8
+ logger.info "No runs found"
9
+ return
10
+ end
11
+
12
+ presenter = CliFormat::Presenter.new(@options)
13
+ presenter.header = ["Id", "Status", "Message", "Created At"]
14
+ runs.each do |item|
15
+ p = ItemPresenter.new(item)
16
+ row = [p.id, p.status, p.message, p.created_at]
17
+ presenter.rows << row
18
+ end
19
+ presenter.show
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,109 @@
1
+ class Terraspace::Terraform::Cloud::Runs
2
+ class Pruner < Base
3
+ include Terraspace::Terraform::Api::Client
4
+
5
+ # @mod required for Api::Client
6
+ def initialize(mod, options={})
7
+ super
8
+ @queue, @kept, @needs_pruning = [], nil, false
9
+ end
10
+
11
+ def run
12
+ build_project
13
+ build_queue
14
+ are_you_sure?
15
+ process_queue
16
+ end
17
+
18
+ private
19
+ def build_queue
20
+ runs.each do |item|
21
+ next unless actionable?(item)
22
+ unless @kept
23
+ @kept = item
24
+ next
25
+ end
26
+ queue(item)
27
+ @needs_pruning = true
28
+ end
29
+ end
30
+
31
+ def are_you_sure?
32
+ unless @needs_pruning
33
+ logger.info "Nothing to prune"
34
+ return
35
+ end
36
+
37
+ keeping = item_message(@kept)
38
+ items = @queue.map { |i| item_message(i) }.join("\n")
39
+ message =<<~EOL
40
+ Will keep:
41
+
42
+ #{keeping}
43
+
44
+ Will prune:
45
+
46
+ #{items}
47
+
48
+ Are you sure?
49
+ EOL
50
+ sure?(message.chop)
51
+ end
52
+
53
+ def item_message(item)
54
+ p = ItemPresenter.new(item)
55
+ " #{p.id} #{p.status} #{p.message} #{p.created_at}"
56
+ end
57
+
58
+ def process_queue
59
+ @queue.each do |item|
60
+ process(item)
61
+ end
62
+ end
63
+
64
+ def process(item)
65
+ id = item['id']
66
+ action = discardable?(item) ? "Discarded" : "Cancelled"
67
+ p = ItemPresenter.new(item)
68
+ msg = "#{action} #{p.id} #{p.message}" # note id is named run-xxx
69
+ logger.info("NOOP: #{msg}") && return if @options[:noop]
70
+
71
+ if discardable?(item)
72
+ api.runs.discard(id)
73
+ elsif cancelable?(item)
74
+ api.runs.cancel(id)
75
+ end
76
+ logger.info msg
77
+ end
78
+
79
+ def queue(item)
80
+ if discardable?(item)
81
+ @queue << item
82
+ elsif cancelable?(item)
83
+ @queue << item
84
+ end
85
+ end
86
+
87
+ # Docs seem to be off: https://www.terraform.io/docs/cloud/api/run.html#apply-a-run
88
+ #
89
+ # This includes runs in the "pending," "needs confirmation," "policy checked," and "policy override" states.
90
+ #
91
+ # Cant really discard a "pending" status, but can discard a "planned" status.
92
+ #
93
+ def actionable?(item)
94
+ discardable?(item) || cancelable?(item)
95
+ end
96
+
97
+ def discardable?(item)
98
+ %w[planned].include?(status(item))
99
+ end
100
+
101
+ def cancelable?(item)
102
+ %w[pending].include?(status(item))
103
+ end
104
+
105
+ def status(item)
106
+ item['attributes']['status']
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,41 @@
1
+ module Terraspace::Terraform::Cloud
2
+ class Sync < Terraspace::CLI::Base
3
+ extend Memoist
4
+ include Terraspace::Terraform::Api::Client
5
+
6
+ # Note about why workspace.create is called:
7
+ #
8
+ # CLI::Init#run
9
+ # init => runs `terraform init`
10
+ # build_remote_dependencies
11
+ # sync_cloud => leads to create_workspace
12
+ #
13
+ # The `terraform init` will auto-create the TFC workspace
14
+ # If there is a .terraform folder the config.init.mode == "auto" though,
15
+ # then the workspace won't be created.
16
+ # So we check and create the workspace if necessary.
17
+ def run
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]
20
+ return unless workspaces_backend?
21
+ logger.info "Syncing to Terraform Cloud: #{@mod.name} => #{workspace_name}"
22
+ @api = Terraspace::Terraform::Api.new(@mod, remote)
23
+ workspace.create_or_update
24
+ workspace.set_working_dir
25
+ workspace.set_env_vars
26
+ end
27
+
28
+ def workspace
29
+ @api.workspace
30
+ end
31
+
32
+ def workspaces_backend?
33
+ remote && remote['workspaces']
34
+ end
35
+
36
+ # already memoized in Api::Client
37
+ def backend
38
+ Terraspace::Compiler::Backend::Parser.new(@mod).result
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,52 @@
1
+ module Terraspace::Terraform::Cloud
2
+ class Syncer < Terraspace::CLI::Base
3
+ extend Memoist
4
+ include Terraspace::Compiler::DirsConcern
5
+ include Terraspace::Util::Sure
6
+
7
+ def run
8
+ are_you_sure?
9
+ mods.each do |mod|
10
+ run_sync(mod)
11
+ end
12
+ end
13
+
14
+ def mods
15
+ mod = @options[:mod]
16
+ mod ? [mod] : stack_names
17
+ end
18
+
19
+ def run_sync(mod)
20
+ sync(mod).run
21
+ end
22
+
23
+ def sync(mod)
24
+ Sync.new(@options.merge(mod: mod))
25
+ end
26
+ memoize :sync
27
+
28
+ def are_you_sure?
29
+ message =<<~EOL
30
+ About to sync these project stacks with Terraform Cloud workspaces:
31
+
32
+ Stack => Workspace
33
+ EOL
34
+
35
+ mods.each do |mod|
36
+ sync = sync(mod)
37
+ message << " #{mod} => #{sync.workspace_name}\n"
38
+ end
39
+ message << <<~EOL
40
+
41
+ A sync does the following for each workspace:
42
+
43
+ 1. Create or update workspace, including the VCS settings.
44
+ 2. Set the working dir.
45
+ 3. Set env and terraform variables.
46
+
47
+ Are you sure?
48
+ EOL
49
+ sure?(message.chop)
50
+ end
51
+ end
52
+ end
@@ -1,8 +1,9 @@
1
- class Terraspace::Terraform::Cloud
1
+ module Terraspace::Terraform::Cloud
2
2
  class Workspace < Terraspace::CLI::Base
3
3
  extend Memoist
4
4
  include Terraspace::Util::Logging
5
5
  include Terraspace::Terraform::Api::Client
6
+ include Terraspace::Terraform::Api::Http::Concern
6
7
 
7
8
  # List will not have @mod set.
8
9
  def list
@@ -32,10 +33,15 @@ class Terraspace::Terraform::Cloud
32
33
  Terraspace::CLI::Init.new(@options.merge(calling_command: "cloud-setup")).run
33
34
  end
34
35
 
36
+ def create
37
+ build
38
+ return unless api
39
+ api.workspace.create
40
+ end
41
+
35
42
  def destroy
36
43
  build
37
- return unless backend.dig('remote','workspaces') # in case called by terraspace down demo -y --destroy-workspace with a non-remote backend
38
- api = Terraspace::Terraform::Api.new(@mod, remote)
44
+ return unless api
39
45
  workspace = api.workspace(exit_on_fail: false)
40
46
  unless workspace
41
47
  logger.info "Workspace #{workspace_name} not found for #{@mod.type}: #{@mod.name}"
@@ -43,25 +49,8 @@ class Terraspace::Terraform::Cloud
43
49
  end
44
50
  sure?
45
51
  logger.info "Destroying workspace #{workspace_name}"
46
- api.destroy_workspace
47
- end
48
-
49
- def build
50
- Terraspace::Builder.new(@options).run
51
- end
52
-
53
- def workspace_name
54
- remote['workspaces']['name']
55
- end
56
-
57
- def remote
58
- backend["remote"]
59
- end
60
-
61
- def backend
62
- Terraspace::Compiler::Backend::Parser.new(@mod).result
52
+ api.workspace.destroy
63
53
  end
64
- memoize :backend
65
54
 
66
55
  def sure?
67
56
  message = <<~EOL.chop + " " # chop to remove newline
@@ -34,7 +34,7 @@ module Terraspace::Terraform::Hooks
34
34
  exit_on_fail = exit_on_fail.nil? ? true : exit_on_fail
35
35
 
36
36
  logger.info "Running #{type} hook"
37
- sh(execute, exit_on_fail: exit_on_fail)
37
+ Terraspace::Shell.new(@mod, execute, exit_on_fail: exit_on_fail).run
38
38
  end
39
39
  end
40
40
  end
@@ -0,0 +1,122 @@
1
+ module Terraspace::Terraform::RemoteState
2
+ class Fetcher
3
+ extend Memoist
4
+ include Terraspace::Util::Logging
5
+
6
+ def initialize(parent, identifier, options={})
7
+ @parent, @identifier, @options = parent, identifier, options
8
+ child_name, @output_key = identifier.split('.')
9
+ @child = Terraspace::Mod.new(child_name)
10
+ end
11
+
12
+ def run
13
+ validate!
14
+ pull
15
+ load
16
+ end
17
+
18
+ def output
19
+ run
20
+ if pull_success?
21
+ value = output_value
22
+ error = output_error(:key_not_found) unless @outputs.key?(@output_key)
23
+ OutputProxy.new(value, @options.merge(error: error))
24
+ else
25
+ error = output_error(:state_not_found)
26
+ OutputProxy.new(nil, @options.merge(error: error))
27
+ end
28
+ end
29
+
30
+ def output_value
31
+ return unless @outputs.key?(@output_key)
32
+ result = @outputs.dig(@output_key)
33
+ result.dig("value") if result
34
+ end
35
+
36
+ def output_error(type)
37
+ msg = case type
38
+ when :key_not_found
39
+ "Output #{@output_key} was not found for the #{@parent.name} tfvars file. Either #{@child.name} stack has not been deployed yet or it does not have this output: #{@output_key}"
40
+ when :state_not_found
41
+ "Output #{@output_key} could not be looked up for the #{@parent.name} tfvars file. #{@child.name} stack needs to be deployed"
42
+ end
43
+ msg = "(#{msg})"
44
+ log_message(msg)
45
+ msg
46
+ end
47
+
48
+ @@pull_successes = {}
49
+ @@download_shown = false
50
+ def pull
51
+ return if @@pull_successes[cache_key]
52
+ logger.info "Downloading tfstate files for dependencies defined in tfvars..." unless @@download_shown || @options[:quiet]
53
+ @@download_shown = true
54
+ logger.debug "Downloading tfstate for stack: #{@child.name}"
55
+ Terraspace::CLI::Init.new(mod: @child.name, calling_command: "apply", quiet: true).init # init not run, so only init
56
+ FileUtils.mkdir_p(File.dirname(state_path))
57
+ command = "cd #{@child.cache_dir} && terraform state pull > #{state_path}"
58
+ logger.debug "=> #{command}"
59
+ success = system(command)
60
+ # Can error if using a old terraform version and the statefile was created with a newer version of terraform
61
+ # IE: Failed to refresh state: state snapshot was created by Terraform v0.13.2, which is newer than current v0.12.29;
62
+ # upgrade to Terraform v0.13.2 or greater to work with this state
63
+ unless success
64
+ logger.info "Error running: #{command}".color(:red)
65
+ logger.info "Please fix the error before continuing"
66
+ end
67
+ @@pull_successes[cache_key] = success
68
+ end
69
+
70
+ def load
71
+ return self unless pull_success?
72
+
73
+ # use or set cache
74
+ if @@cache[cache_key]
75
+ @outputs = @@cache[cache_key]
76
+ else
77
+ @outputs = @@cache[cache_key] = read_statefile_outputs
78
+ end
79
+
80
+ self
81
+ end
82
+ memoize :load
83
+
84
+ def cache_key
85
+ @child.name
86
+ end
87
+
88
+ def read_statefile_outputs
89
+ data = JSON.load(IO.read(state_path))
90
+ data ? data['outputs'] : {}
91
+ end
92
+
93
+ def pull_success?
94
+ @@pull_successes[cache_key]
95
+ end
96
+
97
+ def state_path
98
+ "#{Terraspace.tmp_root}/remote_state/#{@child.build_dir}/state.json"
99
+ end
100
+
101
+ # Note we already validate mod exist at the terraform_output helper. This is just in case that logic changes.
102
+ def validate!
103
+ return if @child.exist?
104
+ logger.error "ERROR: stack #{@child.name} not found".color(:red)
105
+ exit 1
106
+ end
107
+
108
+ # Using debug level because all the tfvar files always get evaluated.
109
+ # So dont want these messages to show up and be noisy unless debugging.
110
+ def log_message(msg)
111
+ logger.debug "DEBUG: #{msg}".color(:yellow)
112
+ end
113
+
114
+ cattr_accessor :cache, default: {}
115
+ class << self
116
+ def flush!
117
+ @@pull_successes = {}
118
+ @@cache = {}
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,39 @@
1
+ module Terraspace::Terraform::RemoteState::Marker
2
+ class Output
3
+ include Terraspace::Util::Logging
4
+
5
+ def initialize(mod, identifier, options={})
6
+ @mod, @identifier, @options = mod, identifier, options
7
+ @parent_name = @mod.name
8
+ @child_name, @output_key = @identifier.split('.')
9
+ end
10
+
11
+ def build
12
+ if valid?
13
+ Terraspace::Dependency::Registry.register(@parent_name, @child_name)
14
+ else
15
+ warning
16
+ end
17
+ # MARKER for debugging. Only appears on 1st pass. Will not see unless changing Terraspace code for debugging.
18
+ marker = "MARKER:terraform_output('#{@identifier}')"
19
+ Terraspace::Terraform::RemoteState::OutputProxy.new(marker, @options)
20
+ end
21
+
22
+ def valid?
23
+ self.class.stack_names.include?(@child_name)
24
+ end
25
+
26
+ def warning
27
+ logger.warn "WARN: The #{@child_name} stack does not exist".color(:yellow)
28
+ caller_line = caller.find { |l| l.include?('.tfvars') }
29
+ source_code = PrettyTracer.new(caller_line).source_code
30
+ logger.info source_code
31
+ end
32
+
33
+ class << self
34
+ extend Memoist
35
+ # Marker::Output uses DirsConcern stack_names to check if stacks are valid
36
+ include Terraspace::Compiler::DirsConcern
37
+ end
38
+ end
39
+ end